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())!; - - this.page.Created += (_, _) => - { - var hwnd = new HWND(taskDialog.Handle); - - // Bring to front - Windows.Win32.PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, - SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE); - Windows.Win32.PInvoke.SetWindowPos(hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, - SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | - SET_WINDOW_POS_FLAGS.SWP_NOMOVE); - Windows.Win32.PInvoke.SetForegroundWindow(hwnd); - Windows.Win32.PInvoke.SetFocus(hwnd); - Windows.Win32.PInvoke.SetActiveWindow(hwnd); - }; - - // Call private "ShowDialogInternal" - var showDialogInternal = typeof(TaskDialog).GetMethod( - "ShowDialogInternal", - BindingFlags.Instance | BindingFlags.NonPublic, - null, - [typeof(IntPtr), typeof(TaskDialogPage), typeof(TaskDialogStartupLocation)], - null); - - var cts = new CancellationTokenSource(); - _ = this.DialogStatePeriodicUpdate(cts.Token); - - showDialogInternal!.Invoke( - taskDialog, - [IntPtr.Zero, this.page, TaskDialogStartupLocation.CenterScreen]); - - Interlocked.Exchange(ref wasGloballyHidden, 1); - cts.Cancel(); + [UnmanagedCallersOnly] + static HRESULT HResultFuncBinder(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam, nint user) => + ((Func)GCHandle.FromIntPtr(user).Target!) + .Invoke(hwnd, msg, wParam, lParam); } } diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 7483b0a27..29016bc69 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -44,7 +44,9 @@ internal static class ServiceManager private static readonly List LoadedServices = new(); #endif - private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); + private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + private static readonly CancellationTokenSource UnloadCancellationTokenSource = new(); private static ManualResetEvent unloadResetEvent = new(false); @@ -126,7 +128,13 @@ internal static class ServiceManager /// Instance of . /// Instance of . /// Instance of . - public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner) + /// Instance of . + public static void InitializeProvidedServices( + Dalamud dalamud, + ReliableFileStorage fs, + DalamudConfiguration configuration, + TargetSigScanner scanner, + Localization localization) { #if DEBUG lock (LoadedServices) @@ -136,6 +144,7 @@ internal static class ServiceManager ProvideService(configuration); ProvideService(new ServiceContainer()); ProvideService(scanner); + ProvideService(localization); } return; @@ -152,6 +161,7 @@ internal static class ServiceManager ProvideService(configuration); ProvideService(new ServiceContainer()); ProvideService(scanner); + ProvideService(localization); return; void ProvideService(T service) where T : IServiceType => Service.Provide(service); @@ -242,19 +252,20 @@ internal static class ServiceManager try { // Wait for all blocking constructors to complete first. - await WaitWithTimeoutConsent(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]), + await WaitWithTimeoutConsent( + blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]), LoadingDialog.State.LoadingDalamud); // All the BlockingEarlyLoadedService constructors have been run, // and blockerTasks now will not change. Now wait for them. // Note that ServiceManager.CallWhenServicesReady does not get to register a blocker. - await WaitWithTimeoutConsent(blockerTasks, + await WaitWithTimeoutConsent( + blockerTasks, LoadingDialog.State.LoadingPlugins); Log.Verbose("=============== BLOCKINGSERVICES & TASKS INITIALIZED ==============="); Timings.Event("BlockingServices Initialized"); BlockingServicesLoadedTaskCompletionSource.SetResult(); - loadingDialog.HideAndJoin(); } catch (Exception e) { @@ -269,11 +280,16 @@ internal static class ServiceManager Log.Error(e, "Failed resolving blocking services"); } + finally + { + loadingDialog.HideAndJoin(); + } return; async Task WaitWithTimeoutConsent(IEnumerable tasksEnumerable, LoadingDialog.State state) { + loadingDialog.CurrentState = state; var tasks = tasksEnumerable.AsReadOnlyCollection(); if (tasks.Count == 0) return; @@ -286,7 +302,6 @@ internal static class ServiceManager { loadingDialog.Show(); loadingDialog.CanHide = true; - loadingDialog.CurrentState = state; } } }).ConfigureAwait(false); diff --git a/Dalamud/Service/Service{T}.cs b/Dalamud/Service/Service{T}.cs index 57acd2ccf..b4bfff917 100644 --- a/Dalamud/Service/Service{T}.cs +++ b/Dalamud/Service/Service{T}.cs @@ -332,7 +332,7 @@ internal static class Service where T : IServiceType break; } - instanceTcs = new TaskCompletionSource(); + instanceTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); instanceTcs.SetException(new UnloadedException()); } diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 514785823..0109339fe 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -85,7 +85,7 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud .Where(x => x is not DalamudAsset.Empty4X4) .Where(x => x.GetAttribute()?.Required is false) .Select(this.CreateStreamAsync) - .Select(x => x.ToContentDisposedTask())) + .Select(x => x.ToContentDisposedTask(true))) .ContinueWith(r => Log.Verbose($"Optional assets load state: {r}")); } diff --git a/Dalamud/Support/CurrentProcessModules.cs b/Dalamud/Support/CurrentProcessModules.cs index e1e3465b3..cd73ceb04 100644 --- a/Dalamud/Support/CurrentProcessModules.cs +++ b/Dalamud/Support/CurrentProcessModules.cs @@ -20,10 +20,18 @@ internal static unsafe partial class CurrentProcessModules { t = 0; process = null; - Log.Verbose("{what}: Fetchling fresh copy of current process modules.", nameof(CurrentProcessModules)); + Log.Verbose("{what}: Fetching fresh copy of current process modules.", nameof(CurrentProcessModules)); } - return (process ??= Process.GetCurrentProcess()).Modules; + try + { + return (process ??= Process.GetCurrentProcess()).Modules; + } + catch (Exception e) + { + Log.Verbose(e, "{what}: Failed to fetch module list.", nameof(CurrentProcessModules)); + return new([]); + } } } diff --git a/Dalamud/Utility/AsyncUtils.cs b/Dalamud/Utility/AsyncUtils.cs index 9533f2ab0..4de561275 100644 --- a/Dalamud/Utility/AsyncUtils.cs +++ b/Dalamud/Utility/AsyncUtils.cs @@ -21,7 +21,7 @@ public static class AsyncUtils /// Returns the first task that completes, according to . public static Task FirstSuccessfulTask(ICollection> tasks) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var remainingTasks = tasks.Count; foreach (var task in tasks) diff --git a/Dalamud/Utility/DynamicPriorityQueueLoader.cs b/Dalamud/Utility/DynamicPriorityQueueLoader.cs index 8109d2e94..83fd366bb 100644 --- a/Dalamud/Utility/DynamicPriorityQueueLoader.cs +++ b/Dalamud/Utility/DynamicPriorityQueueLoader.cs @@ -238,7 +238,7 @@ internal class DynamicPriorityQueueLoader : IDisposable params IDisposable?[] disposables) : base(basis, cancellationToken, disposables) { - this.taskCompletionSource = new(); + this.taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); this.immediateLoadFunction = immediateLoadFunction; }