diff --git a/Dalamud.Boot/Dalamud.Boot.rc b/Dalamud.Boot/Dalamud.Boot.rc
index daa41a282..b46e81caf 100644
--- a/Dalamud.Boot/Dalamud.Boot.rc
+++ b/Dalamud.Boot/Dalamud.Boot.rc
@@ -12,6 +12,24 @@
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// RT_MANIFEST
+//
+
+RT_MANIFEST_THEMES RT_MANIFEST "themes.manifest"
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
/////////////////////////////////////////////////////////////////////////////
// English (United Kingdom) resources
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj
index 298edbcbc..80435cd67 100644
--- a/Dalamud.Boot/Dalamud.Boot.vcxproj
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj
@@ -197,8 +197,11 @@
+
+
+
-
+
\ No newline at end of file
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
index 87eaf6fcc..7c26b28ff 100644
--- a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
@@ -163,4 +163,7 @@
Dalamud.Boot DLL
+
+
+
\ No newline at end of file
diff --git a/Dalamud.Boot/themes.manifest b/Dalamud.Boot/themes.manifest
new file mode 100644
index 000000000..11c048abd
--- /dev/null
+++ b/Dalamud.Boot/themes.manifest
@@ -0,0 +1,9 @@
+
+
+ Windows Forms Common Control manifest
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs
index 9ea96a45c..93de4c64d 100644
--- a/Dalamud/Dalamud.cs
+++ b/Dalamud/Dalamud.cs
@@ -65,7 +65,12 @@ internal sealed class Dalamud : IServiceType
true, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}.json")));
}
- ServiceManager.InitializeProvidedServices(this, fs, configuration, scanner);
+ ServiceManager.InitializeProvidedServices(
+ this,
+ fs,
+ configuration,
+ scanner,
+ Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride));
// Set up FFXIVClientStructs
this.SetupClientStructsResolver(cacheDir);
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 97be8b600..9ed0aa991 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -32,7 +32,6 @@
- true
true
true
portable
diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs
index 1e6fccd8b..fcf33fe28 100644
--- a/Dalamud/EntryPoint.cs
+++ b/Dalamud/EntryPoint.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Dalamud.Common;
using Dalamud.Configuration.Internal;
+using Dalamud.Interface.Internal.Windows;
using Dalamud.Logging.Internal;
using Dalamud.Logging.Retention;
using Dalamud.Plugin.Internal;
@@ -232,6 +233,10 @@ public sealed class EntryPoint
private static void SerilogOnLogLine(object? sender, (string Line, LogEvent LogEvent) ev)
{
+ if (!LoadingDialog.IsGloballyHidden)
+ LoadingDialog.NewLogEntries.Enqueue(ev);
+ ConsoleWindow.NewLogEntries.Enqueue(ev);
+
if (ev.LogEvent.Exception == null)
return;
diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs
index 1aeb42488..bfb58fd3c 100644
--- a/Dalamud/Game/Config/GameConfig.cs
+++ b/Dalamud/Game/Config/GameConfig.cs
@@ -16,10 +16,17 @@ namespace Dalamud.Game.Config;
[ServiceManager.EarlyLoadedService]
internal sealed class GameConfig : IInternalDisposableService, IGameConfig
{
- private readonly TaskCompletionSource tcsInitialization = new();
- private readonly TaskCompletionSource tcsSystem = new();
- private readonly TaskCompletionSource tcsUiConfig = new();
- private readonly TaskCompletionSource tcsUiControl = new();
+ private readonly TaskCompletionSource tcsInitialization =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ private readonly TaskCompletionSource tcsSystem =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ private readonly TaskCompletionSource tcsUiConfig =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ private readonly TaskCompletionSource tcsUiControl =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly GameConfigAddressResolver address = new();
private Hook? configChangeHook;
diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs
index 4f9c8d6c6..07942f780 100644
--- a/Dalamud/Game/Framework.cs
+++ b/Dalamud/Game/Framework.cs
@@ -139,7 +139,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
if (numTicks <= 0)
return Task.CompletedTask;
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken);
return tcs.Task;
}
diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
index c5c4581e7..f03518ada 100644
--- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
+++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
@@ -59,7 +59,7 @@ public sealed class SingleFontChooserDialog : IDisposable
private readonly int counter;
private readonly byte[] fontPreviewText = new byte[2048];
- private readonly TaskCompletionSource tcs = new();
+ private readonly TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly IFontAtlas atlas;
private string popupImGuiName;
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 06a93e453..cbbf63075 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -336,7 +336,7 @@ internal class InterfaceManager : IInternalDisposableService
/// A that resolves once is run.
public Task RunBeforeImGuiRender(Action action)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.runBeforeImGuiRender.Enqueue(
() =>
{
@@ -359,7 +359,7 @@ internal class InterfaceManager : IInternalDisposableService
/// A that resolves once is run.
public Task RunBeforeImGuiRender(Func func)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.runBeforeImGuiRender.Enqueue(
() =>
{
@@ -380,7 +380,7 @@ internal class InterfaceManager : IInternalDisposableService
/// A that resolves once is run.
public Task RunAfterImGuiRender(Action action)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.runAfterImGuiRender.Enqueue(
() =>
{
@@ -403,7 +403,7 @@ internal class InterfaceManager : IInternalDisposableService
/// A that resolves once is run.
public Task RunAfterImGuiRender(Func func)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.runAfterImGuiRender.Enqueue(
() =>
{
@@ -817,8 +817,12 @@ internal class InterfaceManager : IInternalDisposableService
// This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene.
_ = this.dalamudAtlas.BuildFontsAsync();
+ SwapChainHelper.BusyWaitForGameDeviceSwapChain();
+ SwapChainHelper.DetectReShade();
+
try
{
+ // Requires that game window to be there, which will be the case once game swap chain is initialized.
if (Service.Get().WindowIsImmersive)
this.SetImmersiveMode(true);
}
@@ -834,9 +838,6 @@ internal class InterfaceManager : IInternalDisposableService
0,
this.SetCursorDetour);
- SwapChainHelper.BusyWaitForGameDeviceSwapChain();
- SwapChainHelper.DetectReShade();
-
Log.Verbose("===== S W A P C H A I N =====");
this.resizeBuffersHook = Hook.FromAddress(
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
index 8f7c0e36c..f7ce5d145 100644
--- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
@@ -18,7 +18,6 @@ using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
-using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
@@ -39,9 +38,6 @@ internal class ConsoleWindow : Window, IDisposable
private const int LogLinesMaximum = 1000000;
private const int HistorySize = 50;
- // Only this field may be touched from any thread.
- private readonly ConcurrentQueue<(string Line, LogEvent LogEvent)> newLogEntries;
-
// Fields below should be touched only from the main thread.
private readonly RollingList logText;
private readonly RollingList filteredLogEntries;
@@ -94,7 +90,6 @@ internal class ConsoleWindow : Window, IDisposable
this.autoScroll = configuration.LogAutoScroll;
this.autoOpen = configuration.LogOpenAtStartup;
- SerilogEventSink.Instance.LogLine += this.OnLogLine;
Service.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate);
@@ -114,7 +109,6 @@ internal class ConsoleWindow : Window, IDisposable
this.logLinesLimit = configuration.LogLinesLimit;
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
- this.newLogEntries = new();
this.logText = new(limit);
this.filteredLogEntries = new(limit);
@@ -126,6 +120,9 @@ internal class ConsoleWindow : Window, IDisposable
}
}
+ /// Gets the queue where log entries that are not processed yet are stored.
+ public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();
+
///
public override void OnOpen()
{
@@ -136,7 +133,6 @@ internal class ConsoleWindow : Window, IDisposable
///
public void Dispose()
{
- SerilogEventSink.Instance.LogLine -= this.OnLogLine;
this.configuration.DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
if (Service.GetNullable() is { } framework)
framework.Update -= this.FrameworkOnUpdate;
@@ -324,7 +320,7 @@ internal class ConsoleWindow : Window, IDisposable
ImGuiInputTextFlags.CallbackHistory | ImGuiInputTextFlags.CallbackEdit,
this.CommandInputCallback))
{
- this.newLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), [])));
+ NewLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), [])));
this.ProcessCommand();
getFocus = true;
}
@@ -372,7 +368,7 @@ internal class ConsoleWindow : Window, IDisposable
this.pendingClearLog = false;
this.logText.Clear();
this.filteredLogEntries.Clear();
- this.newLogEntries.Clear();
+ NewLogEntries.Clear();
}
if (this.pendingRefilter)
@@ -388,7 +384,7 @@ internal class ConsoleWindow : Window, IDisposable
var numPrevFilteredLogEntries = this.filteredLogEntries.Count;
var addedLines = 0;
- while (this.newLogEntries.TryDequeue(out var logLine))
+ while (NewLogEntries.TryDequeue(out var logLine))
addedLines += this.HandleLogLine(logLine.Line, logLine.LogEvent);
this.newRolledLines = addedLines - (this.filteredLogEntries.Count - numPrevFilteredLogEntries);
}
@@ -1062,11 +1058,6 @@ internal class ConsoleWindow : Window, IDisposable
/// Queues filtering the log entries again, before next call to .
private void QueueRefilter() => this.pendingRefilter = true;
- /// Enqueues the new log line to the log-to-be-processed queue.
- /// See for the handler for the queued log entries.
- private void OnLogLine(object sender, (string Line, LogEvent LogEvent) logEvent) =>
- this.newLogEntries.Enqueue(logEvent);
-
private bool DrawToggleButtonWithTooltip(
string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState)
{
diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
index e67ff3cf5..e95d2e1b8 100644
--- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
@@ -315,7 +315,7 @@ internal class PluginImageCache : IInternalDisposableService
private Task RunInDownloadQueue(Func> func, ulong requestedFrame)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.downloadQueue.Add(Tuple.Create(requestedFrame, async () =>
{
try
@@ -332,7 +332,7 @@ internal class PluginImageCache : IInternalDisposableService
private Task RunInLoadQueue(Func> func)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.loadQueue.Add(async () =>
{
try
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
index 466277a2f..ccf7b8226 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
@@ -3774,7 +3774,7 @@ internal class PluginInstallerWindow : Window, IDisposable
this.errorModalMessage = message;
this.errorModalDrawing = true;
this.errorModalOnNextFrame = true;
- this.errorModalTaskCompletionSource = new TaskCompletionSource();
+ this.errorModalTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
return this.errorModalTaskCompletionSource.Task;
}
@@ -3782,7 +3782,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{
this.updateModalOnNextFrame = true;
this.updateModalPlugin = plugin;
- this.updateModalTaskCompletionSource = new TaskCompletionSource();
+ this.updateModalTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
return this.updateModalTaskCompletionSource.Task;
}
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs
index ef92ffd65..61ac00faf 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs
@@ -497,7 +497,7 @@ internal sealed partial class FontAtlasFactory
$"{nameof(FontAtlasAutoRebuildMode.Async)}.");
}
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
try
{
var rebuildIndex = Interlocked.Increment(ref this.buildIndex);
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
index 0e26145f0..b84a857da 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
@@ -242,7 +242,7 @@ internal abstract class FontHandle : IFontHandle
if (this.Available)
return Task.FromResult(this);
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.ImFontChanged += OnImFontChanged;
this.Disposed += OnDisposed;
if (this.Available)
diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs
index ad3188925..6e21bc0e8 100644
--- a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs
+++ b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs
@@ -24,7 +24,8 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos
private readonly string? debugName;
private readonly LocalPlugin? ownerPlugin;
private readonly CancellationToken cancellationToken;
- private readonly TaskCompletionSource firstUpdateTaskCompletionSource = new();
+ private readonly TaskCompletionSource firstUpdateTaskCompletionSource =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
private ImGuiViewportTextureArgs args;
private D3D11_TEXTURE2D_DESC desc;
diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
index 2ff42bc2a..a6584f9aa 100644
--- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
+++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs
@@ -59,7 +59,7 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
{
var first = true;
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
Service.Get().Draw += DrawChoices;
encoder = await tcs.Task;
@@ -108,7 +108,7 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
string path;
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
this.fileDialogManager.SaveFileDialog(
"Save texture...",
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs
index 673dba29b..7a0b4347d 100644
--- a/Dalamud/IoC/Internal/ServiceContainer.cs
+++ b/Dalamud/IoC/Internal/ServiceContainer.cs
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Threading;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
@@ -127,7 +128,26 @@ internal class ServiceContainer : IServiceProvider, IServiceType
return null;
}
- ctor.Invoke(instance, resolvedParams);
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var thr = new Thread(
+ () =>
+ {
+ try
+ {
+ ctor.Invoke(instance, resolvedParams);
+ }
+ catch (Exception e)
+ {
+ tcs.SetException(e);
+ return;
+ }
+
+ tcs.SetResult();
+ });
+
+ thr.Start();
+ await tcs.Task.ConfigureAwait(false);
+ thr.Join();
return instance;
}
diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs
index 3ed2ad519..84e8437b3 100644
--- a/Dalamud/Localization.cs
+++ b/Dalamud/Localization.cs
@@ -4,7 +4,6 @@ using System.Linq;
using System.Reflection;
using CheapLoc;
-using Dalamud.Configuration.Internal;
using Serilog;
@@ -13,7 +12,7 @@ namespace Dalamud;
///
/// Class handling localization.
///
-[ServiceManager.EarlyLoadedService]
+[ServiceManager.ProvidedService]
public class Localization : IServiceType
{
///
@@ -43,16 +42,6 @@ public class Localization : IServiceType
this.assembly = Assembly.GetCallingAssembly();
}
- [ServiceManager.ServiceConstructor]
- private Localization(Dalamud dalamud, DalamudConfiguration configuration)
- : this(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_")
- {
- if (!string.IsNullOrEmpty(configuration.LanguageOverride))
- this.SetupWithLangCode(configuration.LanguageOverride);
- else
- this.SetupWithUiCulture();
- }
-
///
/// Delegate for the event that occurs when the language is changed.
///
@@ -167,6 +156,22 @@ public class Localization : IServiceType
Loc.ExportLocalizableForAssembly(this.assembly, ignoreInvalidFunctions);
}
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// Path to Dalamud assets.
+ /// Optional language override.
+ /// A new instance.
+ internal static Localization FromAssets(string assetDirectory, string? languageOverride)
+ {
+ var t = new Localization(Path.Combine(assetDirectory, "UIRes", "loc", "dalamud"), "dalamud_");
+ if (!string.IsNullOrEmpty(languageOverride))
+ t.SetupWithLangCode(languageOverride);
+ else
+ t.SetupWithUiCulture();
+ return t;
+ }
+
private string ReadLocData(string langCode)
{
if (this.useEmbedded)
diff --git a/Dalamud/Service/LoadingDialog.cs b/Dalamud/Service/LoadingDialog.cs
index 64af02171..f788ffb71 100644
--- a/Dalamud/Service/LoadingDialog.cs
+++ b/Dalamud/Service/LoadingDialog.cs
@@ -1,34 +1,46 @@
-using System.Drawing;
+using System.Collections.Concurrent;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
using System.Threading;
-using System.Threading.Tasks;
-using System.Windows.Forms;
+
+using CheapLoc;
using Dalamud.Plugin.Internal;
using Dalamud.Utility;
-using Windows.Win32.Foundation;
-using Windows.Win32.UI.WindowsAndMessaging;
+
+using Serilog;
+using Serilog.Events;
+
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.TASKDIALOG_FLAGS;
+using static TerraFX.Interop.Windows.Windows;
namespace Dalamud;
///
/// Class providing an early-loading dialog.
///
-internal class LoadingDialog
+[SuppressMessage(
+ "StyleCop.CSharp.LayoutRules",
+ "SA1519:Braces should not be omitted from multi-line child statement",
+ Justification = "Multiple fixed blocks")]
+internal sealed unsafe class LoadingDialog
{
- // TODO: We can't localize any of what's in here at the moment, because Localization is an EarlyLoadedService.
-
- private static int wasGloballyHidden = 0;
-
+ private readonly RollingList logs = new(20);
+
private Thread? thread;
- private TaskDialogButton? inProgressHideButton;
- private TaskDialogPage? page;
- private bool canHide;
- private State currentState = State.LoadingDalamud;
+ private HWND hwndTaskDialog;
private DateTime firstShowTime;
-
+ private State currentState = State.LoadingDalamud;
+ private bool canHide;
+
///
/// Enum representing the state of the dialog.
///
@@ -38,18 +50,25 @@ internal class LoadingDialog
/// Show a message stating that Dalamud is currently loading.
///
LoadingDalamud,
-
+
///
/// Show a message stating that Dalamud is currently loading plugins.
///
LoadingPlugins,
-
+
///
/// Show a message stating that Dalamud is currently updating plugins.
///
AutoUpdatePlugins,
}
-
+
+ /// Gets the queue where log entries that are not processed yet are stored.
+ public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();
+
+ /// Gets a value indicating whether the initial Dalamud loading dialog will not show again until next
+ /// game restart.
+ public static bool IsGloballyHidden { get; private set; }
+
///
/// Gets or sets the current state of the dialog.
///
@@ -58,13 +77,16 @@ internal class LoadingDialog
get => this.currentState;
set
{
+ if (this.currentState == value)
+ return;
+
this.currentState = value;
- this.UpdatePage();
+ this.UpdateMainInstructionText();
}
}
-
+
///
- /// Gets or sets a value indicating whether or not the dialog can be hidden by the user.
+ /// Gets or sets a value indicating whether the dialog can be hidden by the user.
///
/// Thrown if called before the dialog has been created.
public bool CanHide
@@ -72,8 +94,11 @@ internal class LoadingDialog
get => this.canHide;
set
{
+ if (this.canHide == value)
+ return;
+
this.canHide = value;
- this.UpdatePage();
+ this.UpdateButtonEnabled();
}
}
@@ -82,19 +107,19 @@ internal class LoadingDialog
///
public void Show()
{
- if (Volatile.Read(ref wasGloballyHidden) == 1)
+ if (IsGloballyHidden)
return;
-
+
if (this.thread?.IsAlive == true)
return;
-
+
this.thread = new Thread(this.ThreadStart)
{
Name = "Dalamud Loading Dialog",
};
this.thread.SetApartmentState(ApartmentState.STA);
this.thread.Start();
-
+
this.firstShowTime = DateTime.Now;
}
@@ -103,150 +128,287 @@ internal class LoadingDialog
///
public void HideAndJoin()
{
- if (this.thread == null || !this.thread.IsAlive)
+ IsGloballyHidden = true;
+ if (this.thread?.IsAlive is not true)
return;
-
- this.inProgressHideButton?.PerformClick();
- this.thread!.Join();
+
+ SendMessageW(this.hwndTaskDialog, WM.WM_CLOSE, default, default);
+ this.thread.Join();
}
- private void UpdatePage()
+ private void UpdateMainInstructionText()
{
- if (this.page == null)
+ if (this.hwndTaskDialog == default)
return;
- this.page.Heading = this.currentState switch
+ fixed (void* pszText = this.currentState switch
+ {
+ State.LoadingDalamud => Loc.Localize(
+ "LoadingDialogMainInstructionLoadingDalamud",
+ "Dalamud is loading..."),
+ State.LoadingPlugins => Loc.Localize(
+ "LoadingDialogMainInstructionLoadingPlugins",
+ "Waiting for plugins to load..."),
+ State.AutoUpdatePlugins => Loc.Localize(
+ "LoadingDialogMainInstructionAutoUpdatePlugins",
+ "Updating plugins..."),
+ _ => string.Empty, // should not happen
+ })
{
- State.LoadingDalamud => "Dalamud is loading...",
- State.LoadingPlugins => "Waiting for plugins to load...",
- State.AutoUpdatePlugins => "Updating plugins...",
- _ => throw new ArgumentOutOfRangeException(),
- };
+ SendMessageW(
+ this.hwndTaskDialog,
+ (uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT,
+ (WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_MAIN_INSTRUCTION,
+ (LPARAM)pszText);
+ }
+ }
- var context = string.Empty;
- if (this.currentState == State.LoadingPlugins)
+ private void UpdateContentText()
+ {
+ if (this.hwndTaskDialog == default)
+ return;
+
+ var contentBuilder = new StringBuilder(
+ Loc.Localize(
+ "LoadingDialogContentInfo",
+ "Some of the plugins you have installed through Dalamud are taking a long time to load.\n" +
+ "This is likely normal, please wait a little while longer."));
+
+ if (this.CurrentState == State.LoadingPlugins)
{
- context = "\nPreparing...";
-
var tracker = Service.GetNullable()?.StartupLoadTracking;
if (tracker != null)
{
- var nameString = tracker.GetPendingInternalNames()
- .Select(x => tracker.GetPublicName(x))
- .Where(x => x != null)
- .Aggregate(string.Empty, (acc, x) => acc + x + ", ");
-
+ var nameString = string.Join(
+ ", ",
+ tracker.GetPendingInternalNames()
+ .Select(x => tracker.GetPublicName(x))
+ .Where(x => x != null));
+
if (!nameString.IsNullOrEmpty())
- context = $"\nWaiting for: {nameString[..^2]}";
+ {
+ contentBuilder
+ .AppendLine()
+ .AppendLine()
+ .Append(
+ string.Format(
+ Loc.Localize("LoadingDialogContentCurrentPlugin", "Waiting for: {0}"),
+ nameString));
+ }
}
}
-
+
// Add some text if loading takes more than a few minutes
if (DateTime.Now - this.firstShowTime > TimeSpan.FromMinutes(3))
- context += "\nIt's been a while now. Please report this issue on our Discord server.";
-
- this.page.Text = this.currentState switch
{
- State.LoadingDalamud => "Please wait while Dalamud loads...",
- State.LoadingPlugins => "Please wait while Dalamud loads plugins...",
- State.AutoUpdatePlugins => "Please wait while Dalamud updates your plugins...",
- _ => throw new ArgumentOutOfRangeException(),
-#pragma warning disable SA1513
- } + context;
-#pragma warning restore SA1513
-
- this.inProgressHideButton!.Enabled = this.canHide;
- }
-
- private async Task DialogStatePeriodicUpdate(CancellationToken token)
- {
- using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(50));
- while (!token.IsCancellationRequested)
- {
- await timer.WaitForNextTickAsync(token);
- this.UpdatePage();
+ contentBuilder
+ .AppendLine()
+ .AppendLine()
+ .Append(
+ Loc.Localize(
+ "LoadingDialogContentTakingTooLong",
+ "It's been a while now. Please report this issue on our Discord server."));
}
+
+ fixed (void* pszText = contentBuilder.ToString())
+ {
+ SendMessageW(
+ this.hwndTaskDialog,
+ (uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT,
+ (WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_CONTENT,
+ (LPARAM)pszText);
+ }
+ }
+
+ private void UpdateExpandedInformation()
+ {
+ const int maxCharactersPerLine = 80;
+
+ if (NewLogEntries.IsEmpty)
+ return;
+
+ var sb = new StringBuilder();
+ while (NewLogEntries.TryDequeue(out var e))
+ {
+ var t = e.Line.AsSpan();
+ var first = true;
+ while (!t.IsEmpty)
+ {
+ var i = t.IndexOfAny('\r', '\n');
+ var line = i == -1 ? t : t[..i];
+ t = i == -1 ? ReadOnlySpan.Empty : t[(i + 1)..];
+ if (line.IsEmpty)
+ continue;
+
+ sb.Clear();
+ if (first)
+ sb.Append($"{e.LogEvent.Timestamp:HH:mm:ss} | ");
+ else
+ sb.Append(" | ");
+ first = false;
+ if (line.Length < maxCharactersPerLine)
+ sb.Append(line);
+ else
+ sb.Append($"{line[..(maxCharactersPerLine - 3)]}...");
+ this.logs.Add(sb.ToString());
+ }
+ }
+
+ sb.Clear();
+ foreach (var l in this.logs)
+ sb.AppendLine(l);
+
+ fixed (void* pszText = sb.ToString())
+ {
+ SendMessageW(
+ this.hwndTaskDialog,
+ (uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT,
+ (WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_EXPANDED_INFORMATION,
+ (LPARAM)pszText);
+ }
+ }
+
+ private void UpdateButtonEnabled()
+ {
+ if (this.hwndTaskDialog == default)
+ return;
+
+ SendMessageW(this.hwndTaskDialog, (uint)TASKDIALOG_MESSAGES.TDM_ENABLE_BUTTON, IDOK, this.canHide ? 1 : 0);
+ }
+
+ private HRESULT TaskDialogCallback(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
+ {
+ switch ((TASKDIALOG_NOTIFICATIONS)msg)
+ {
+ case TASKDIALOG_NOTIFICATIONS.TDN_CREATED:
+ this.hwndTaskDialog = hwnd;
+
+ this.UpdateMainInstructionText();
+ this.UpdateContentText();
+ this.UpdateExpandedInformation();
+ this.UpdateButtonEnabled();
+ SendMessageW(hwnd, (int)TASKDIALOG_MESSAGES.TDM_SET_PROGRESS_BAR_MARQUEE, 1, 0);
+
+ // Bring to front
+ SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SWP.SWP_NOSIZE | SWP.SWP_NOMOVE);
+ SetWindowPos(hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SWP.SWP_NOSIZE | SWP.SWP_NOMOVE);
+ ShowWindow(hwnd, SW.SW_SHOW);
+ SetForegroundWindow(hwnd);
+ SetFocus(hwnd);
+ SetActiveWindow(hwnd);
+ return S.S_OK;
+
+ case TASKDIALOG_NOTIFICATIONS.TDN_DESTROYED:
+ this.hwndTaskDialog = default;
+ return S.S_OK;
+
+ case TASKDIALOG_NOTIFICATIONS.TDN_TIMER:
+ this.UpdateContentText();
+ this.UpdateExpandedInformation();
+ return S.S_OK;
+ }
+
+ return S.S_OK;
}
private void ThreadStart()
{
- Application.EnableVisualStyles();
-
- this.inProgressHideButton = new TaskDialogButton("Hide", this.canHide);
-
// We don't have access to the asset service here.
var workingDirectory = Service.Get().StartInfo.WorkingDirectory;
- TaskDialogIcon? dialogIcon = null;
- if (!workingDirectory.IsNullOrEmpty())
+ using var extractedIcon =
+ string.IsNullOrEmpty(workingDirectory)
+ ? null
+ : Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe"));
+
+ fixed (void* pszEmpty = "-")
+ fixed (void* pszWindowTitle = "Dalamud")
+ fixed (void* pszDalamudBoot = "Dalamud.Boot.dll")
+ fixed (void* pszThemesManifestResourceName = "RT_MANIFEST_THEMES")
+ fixed (void* pszHide = Loc.Localize("LoadingDialogHide", "Hide"))
+ fixed (void* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs"))
+ fixed (void* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs"))
{
- var extractedIcon = Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe"));
- if (extractedIcon != null)
+ var taskDialogButton = new TASKDIALOG_BUTTON
{
- dialogIcon = new TaskDialogIcon(extractedIcon);
+ nButtonID = IDOK,
+ pszButtonText = (ushort*)pszHide,
+ };
+ var taskDialogConfig = new TASKDIALOGCONFIG
+ {
+ cbSize = (uint)sizeof(TASKDIALOGCONFIG),
+ hwndParent = default,
+ hInstance = (HINSTANCE)Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().ManifestModule),
+ dwFlags = (int)TDF_CAN_BE_MINIMIZED |
+ (int)TDF_SHOW_MARQUEE_PROGRESS_BAR |
+ (int)TDF_EXPAND_FOOTER_AREA |
+ (int)TDF_CALLBACK_TIMER |
+ (extractedIcon is null ? 0 : (int)TDF_USE_HICON_MAIN),
+ dwCommonButtons = 0,
+ pszWindowTitle = (ushort*)pszWindowTitle,
+ pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (ushort*)extractedIcon.Handle,
+ pszMainInstruction = null,
+ pszContent = null,
+ cButtons = 1,
+ pButtons = &taskDialogButton,
+ nDefaultButton = IDOK,
+ cRadioButtons = 0,
+ pRadioButtons = null,
+ nDefaultRadioButton = 0,
+ pszVerificationText = null,
+ pszExpandedInformation = (ushort*)pszEmpty,
+ pszExpandedControlText = (ushort*)pszShowLatestLogs,
+ pszCollapsedControlText = (ushort*)pszHideLatestLogs,
+ pszFooterIcon = null,
+ pszFooter = null,
+ pfCallback = &HResultFuncBinder,
+ lpCallbackData = 0,
+ cxWidth = 360,
+ };
+
+ HANDLE hActCtx = default;
+ GCHandle gch = default;
+ nuint cookie = 0;
+ try
+ {
+ var actctx = new ACTCTXW
+ {
+ cbSize = (uint)sizeof(ACTCTXW),
+ dwFlags = ACTCTX_FLAG_HMODULE_VALID | ACTCTX_FLAG_RESOURCE_NAME_VALID,
+ lpResourceName = (ushort*)pszThemesManifestResourceName,
+ hModule = GetModuleHandleW((ushort*)pszDalamudBoot),
+ };
+ hActCtx = CreateActCtxW(&actctx);
+ if (hActCtx == default)
+ throw new Win32Exception("CreateActCtxW failure.");
+
+ if (!ActivateActCtx(hActCtx, &cookie))
+ throw new Win32Exception("ActivateActCtx failure.");
+
+ gch = GCHandle.Alloc((Func)this.TaskDialogCallback);
+ taskDialogConfig.lpCallbackData = GCHandle.ToIntPtr(gch);
+ TaskDialogIndirect(&taskDialogConfig, null, null, null).ThrowOnError();
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "TaskDialogIndirect failure.");
+ }
+ finally
+ {
+ if (gch.IsAllocated)
+ gch.Free();
+ if (cookie != 0)
+ DeactivateActCtx(0, cookie);
+ ReleaseActCtx(hActCtx);
}
}
- dialogIcon ??= TaskDialogIcon.Information;
- this.page = new TaskDialogPage
- {
- ProgressBar = new TaskDialogProgressBar(TaskDialogProgressBarState.Marquee),
- Caption = "Dalamud",
- Icon = dialogIcon,
- Buttons = { this.inProgressHideButton },
- AllowMinimize = false,
- AllowCancel = false,
- Expander = new TaskDialogExpander
- {
- CollapsedButtonText = "What does this mean?",
- ExpandedButtonText = "What does this mean?",
- Text = "Some of the plugins you have installed through Dalamud are taking a long time to load.\n" +
- "This is likely normal, please wait a little while longer.",
- },
- SizeToContent = true,
- };
-
- this.UpdatePage();
+ IsGloballyHidden = true;
- // Call private TaskDialog ctor
- var ctor = typeof(TaskDialog).GetConstructor(
- BindingFlags.Instance | BindingFlags.NonPublic,
- null,
- Array.Empty(),
- null);
+ return;
- var taskDialog = (TaskDialog)ctor!.Invoke(Array.Empty