diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index f6ac5b151..01634b328 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -111,10 +111,6 @@
-
-
-
-
diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
new file mode 100644
index 000000000..14def2036
--- /dev/null
+++ b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
@@ -0,0 +1,107 @@
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+namespace Dalamud.Game.Addon;
+
+/// Argument pool for Addon Lifecycle services.
+[ServiceManager.EarlyLoadedService]
+internal sealed class AddonLifecyclePooledArgs : IServiceType
+{
+ private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64];
+ private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64];
+ private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64];
+ private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64];
+ private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64];
+ private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64];
+ private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64];
+
+ [ServiceManager.ServiceConstructor]
+ private AddonLifecyclePooledArgs()
+ {
+ }
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonRequestedUpdateArgs arg) =>
+ new(out arg, this.addonRequestedUpdateArgPool);
+
+ /// Rents an instance of an argument.
+ /// The rented instance.
+ /// The returner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public PooledEntry Rent(out AddonReceiveEventArgs arg) =>
+ new(out arg, this.addonReceiveEventArgPool);
+
+ /// Returns the object to the pool on dispose.
+ /// The type.
+ public readonly ref struct PooledEntry
+ where T : AddonArgs, new()
+ {
+ private readonly Span pool;
+ private readonly T obj;
+
+ /// Initializes a new instance of the struct.
+ /// An instance of the argument.
+ /// The pool to rent from and return to.
+ public PooledEntry(out T arg, Span pool)
+ {
+ this.pool = pool;
+ foreach (ref var item in pool)
+ {
+ if (Interlocked.Exchange(ref item, null) is { } v)
+ {
+ this.obj = arg = v;
+ return;
+ }
+ }
+
+ this.obj = arg = new();
+ }
+
+ /// Returns the item to the pool.
+ public void Dispose()
+ {
+ var tmp = this.obj;
+ foreach (ref var item in this.pool)
+ {
+ if (Interlocked.Exchange(ref item, tmp) is not { } tmp2)
+ return;
+ tmp = tmp2;
+ }
+ }
+ }
+}
diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs
index 4231b0d09..8ee09bed8 100644
--- a/Dalamud/Game/Addon/Events/AddonEventManager.cs
+++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs
@@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Events;
/// Service provider for addon event management.
///
[InterfaceVersion("1.0")]
-[ServiceManager.BlockingEarlyLoadedService]
+[ServiceManager.EarlyLoadedService]
internal unsafe class AddonEventManager : IDisposable, IServiceType
{
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
index 4ab3de5ca..1095202cc 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
@@ -44,10 +44,10 @@ public abstract unsafe class AddonArgs
get => this.addon;
set
{
- if (this.addon == value)
- return;
-
this.addon = value;
+
+ // Note: always clear addonName on updating the addon being pointed.
+ // Same address may point to a different addon.
this.addonName = null;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index beaab7fcd..37f12ce3a 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -1,4 +1,3 @@
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -19,7 +18,7 @@ namespace Dalamud.Game.Addon.Lifecycle;
/// This class provides events for in-game addon lifecycles.
///
[InterfaceVersion("1.0")]
-[ServiceManager.BlockingEarlyLoadedService]
+[ServiceManager.EarlyLoadedService]
internal unsafe class AddonLifecycle : IDisposable, IServiceType
{
private static readonly ModuleLog Log = new("AddonLifecycle");
@@ -27,6 +26,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service.Get();
+ [ServiceManager.ServiceDependency]
+ private readonly AddonLifecyclePooledArgs argsPool = Service.Get();
+
private readonly nint disallowedReceiveEventAddress;
private readonly AddonLifecycleAddressResolver address;
@@ -38,18 +40,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
private readonly Hook onAddonRefreshHook;
private readonly CallHook onAddonRequestedUpdateHook;
- // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet
- // package, and these events are always called from the main thread, this is fine.
-#pragma warning disable CS0618 // Type or member is obsolete
- // TODO: turn constructors of these internal
- private readonly AddonSetupArgs recyclingSetupArgs = new();
- private readonly AddonFinalizeArgs recyclingFinalizeArgs = new();
- private readonly AddonDrawArgs recyclingDrawArgs = new();
- private readonly AddonUpdateArgs recyclingUpdateArgs = new();
- private readonly AddonRefreshArgs recyclingRefreshArgs = new();
- private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new();
-#pragma warning restore CS0618 // Type or member is obsolete
-
[ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner)
{
@@ -253,12 +243,13 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
}
- this.recyclingSetupArgs.AddonInternal = (nint)addon;
- this.recyclingSetupArgs.AtkValueCount = valueCount;
- this.recyclingSetupArgs.AtkValues = (nint)values;
- this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs);
- valueCount = this.recyclingSetupArgs.AtkValueCount;
- values = (AtkValue*)this.recyclingSetupArgs.AtkValues;
+ using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
+ arg.AddonInternal = (nint)addon;
+ arg.AtkValueCount = valueCount;
+ arg.AtkValues = (nint)values;
+ this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
+ valueCount = arg.AtkValueCount;
+ values = (AtkValue*)arg.AtkValues;
try
{
@@ -269,7 +260,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
}
- this.InvokeListenersSafely(AddonEvent.PostSetup, this.recyclingSetupArgs);
+ this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
}
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
@@ -284,8 +275,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
}
- this.recyclingFinalizeArgs.AddonInternal = (nint)atkUnitBase[0];
- this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs);
+ using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
+ arg.AddonInternal = (nint)atkUnitBase[0];
+ this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
try
{
@@ -299,8 +291,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
private void OnAddonDraw(AtkUnitBase* addon)
{
- this.recyclingDrawArgs.AddonInternal = (nint)addon;
- this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs);
+ using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
+ arg.AddonInternal = (nint)addon;
+ this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
try
{
@@ -311,14 +304,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
}
- this.InvokeListenersSafely(AddonEvent.PostDraw, this.recyclingDrawArgs);
+ this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
- this.recyclingUpdateArgs.AddonInternal = (nint)addon;
- this.recyclingUpdateArgs.TimeDeltaInternal = delta;
- this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs);
+ using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
+ arg.AddonInternal = (nint)addon;
+ arg.TimeDeltaInternal = delta;
+ this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
try
{
@@ -329,19 +323,20 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
}
- this.InvokeListenersSafely(AddonEvent.PostUpdate, this.recyclingUpdateArgs);
+ this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
}
private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
byte result = 0;
- this.recyclingRefreshArgs.AddonInternal = (nint)addon;
- this.recyclingRefreshArgs.AtkValueCount = valueCount;
- this.recyclingRefreshArgs.AtkValues = (nint)values;
- this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs);
- valueCount = this.recyclingRefreshArgs.AtkValueCount;
- values = (AtkValue*)this.recyclingRefreshArgs.AtkValues;
+ using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
+ arg.AddonInternal = (nint)addon;
+ arg.AtkValueCount = valueCount;
+ arg.AtkValues = (nint)values;
+ this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
+ valueCount = arg.AtkValueCount;
+ values = (AtkValue*)arg.AtkValues;
try
{
@@ -352,18 +347,19 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
}
- this.InvokeListenersSafely(AddonEvent.PostRefresh, this.recyclingRefreshArgs);
+ this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
- this.recyclingRequestedUpdateArgs.AddonInternal = (nint)addon;
- this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
- this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData;
- this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs);
- numberArrayData = (NumberArrayData**)this.recyclingRequestedUpdateArgs.NumberArrayData;
- stringArrayData = (StringArrayData**)this.recyclingRequestedUpdateArgs.StringArrayData;
+ using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
+ arg.AddonInternal = (nint)addon;
+ arg.NumberArrayData = (nint)numberArrayData;
+ arg.StringArrayData = (nint)stringArrayData;
+ this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
+ numberArrayData = (NumberArrayData**)arg.NumberArrayData;
+ stringArrayData = (StringArrayData**)arg.StringArrayData;
try
{
@@ -374,7 +370,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
- this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.recyclingRequestedUpdateArgs);
+ this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
index 43aa71661..fd3b5d79d 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
@@ -16,12 +16,8 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
{
private static readonly ModuleLog Log = new("AddonLifecycle");
- // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet
- // package, and these events are always called from the main thread, this is fine.
-#pragma warning disable CS0618 // Type or member is obsolete
- // TODO: turn constructors of these internal
- private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new();
-#pragma warning restore CS0618 // Type or member is obsolete
+ [ServiceManager.ServiceDependency]
+ private readonly AddonLifecyclePooledArgs argsPool = Service.Get();
///
/// Initializes a new instance of the class.
@@ -82,16 +78,17 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
return;
}
- this.recyclingReceiveEventArgs.AddonInternal = (nint)addon;
- this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType;
- this.recyclingReceiveEventArgs.EventParam = eventParam;
- this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent;
- this.recyclingReceiveEventArgs.Data = data;
- this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.recyclingReceiveEventArgs);
- eventType = (AtkEventType)this.recyclingReceiveEventArgs.AtkEventType;
- eventParam = this.recyclingReceiveEventArgs.EventParam;
- atkEvent = (AtkEvent*)this.recyclingReceiveEventArgs.AtkEvent;
- data = this.recyclingReceiveEventArgs.Data;
+ using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
+ arg.AddonInternal = (nint)addon;
+ arg.AtkEventType = (byte)eventType;
+ arg.EventParam = eventParam;
+ arg.AtkEvent = (IntPtr)atkEvent;
+ arg.Data = data;
+ this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
+ eventType = (AtkEventType)arg.AtkEventType;
+ eventParam = arg.EventParam;
+ atkEvent = (AtkEvent*)arg.AtkEvent;
+ data = arg.Data;
try
{
@@ -102,6 +99,6 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
- this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.recyclingReceiveEventArgs);
+ this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
}
}
diff --git a/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs b/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs
index 63a5b828a..0c5d16675 100644
--- a/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs
+++ b/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs
@@ -1,6 +1,7 @@
using System;
using Dalamud.Game.ClientState.Statuses;
+using Dalamud.Utility;
namespace Dalamud.Game.ClientState.Objects.Types;
@@ -57,8 +58,22 @@ public unsafe class BattleChara : Character
///
/// Gets the total casting time of the spell being cast by the chara.
///
+ ///
+ /// This can only be a portion of the total cast for some actions.
+ /// Use AdjustedTotalCastTime if you always need the total cast time.
+ ///
+ [Api10ToDo("Rename so it is not confused with AdjustedTotalCastTime")]
public float TotalCastTime => this.Struct->GetCastInfo->TotalCastTime;
+ ///
+ /// Gets the plus any adjustments from the game, such as Action offset 2B. Used for display purposes.
+ ///
+ ///
+ /// This is the actual total cast time for all actions.
+ ///
+ [Api10ToDo("Rename so it is not confused with TotalCastTime")]
+ public float AdjustedTotalCastTime => this.Struct->GetCastInfo->AdjustedTotalCastTime;
+
///
/// Gets the underlying structure.
///
diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs
index 76d3b5659..8d5ec1344 100644
--- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs
+++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs
@@ -12,6 +12,7 @@ using Dalamud.Game.Gui;
using Dalamud.Game.Network.Internal.MarketBoardUploaders;
using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis;
using Dalamud.Game.Network.Structures;
+using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Networking.Http;
using Dalamud.Utility;
@@ -268,8 +269,8 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType
return result;
}
- var cfcName = cfCondition.Name.ToString();
- if (cfcName.IsNullOrEmpty())
+ var cfcName = cfCondition.Name.ToDalamudString();
+ if (cfcName.Payloads.Count == 0)
{
cfcName = "Duty Roulette";
cfCondition.Image = 112324;
@@ -279,7 +280,10 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType
{
if (this.configuration.DutyFinderChatMessage)
{
- Service.GetNullable()?.Print($"Duty pop: {cfcName}");
+ var b = new SeStringBuilder();
+ b.Append("Duty pop: ");
+ b.Append(cfcName);
+ Service.GetNullable()?.Print(b.Build());
}
this.CfPop.InvokeSafely(cfCondition);
diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs
index ace8887f1..b64df8f19 100644
--- a/Dalamud/Interface/Internal/DalamudCommands.cs
+++ b/Dalamud/Interface/Internal/DalamudCommands.cs
@@ -96,12 +96,6 @@ internal class DalamudCommands : IServiceType
ShowInHelp = false,
});
- commandManager.AddHandler("/xlime", new CommandInfo(this.OnDebugDrawIMEPanel)
- {
- HelpMessage = Loc.Localize("DalamudIMEPanelHelp", "Draw IME panel"),
- ShowInHelp = false,
- });
-
commandManager.AddHandler("/xllog", new CommandInfo(this.OnOpenLog)
{
HelpMessage = Loc.Localize("DalamudDevLogHelp", "Open dev log DEBUG"),
@@ -308,11 +302,6 @@ internal class DalamudCommands : IServiceType
dalamudInterface.ToggleDataWindow(arguments);
}
- private void OnDebugDrawIMEPanel(string command, string arguments)
- {
- Service.Get().OpenImeWindow();
- }
-
private void OnOpenLog(string command, string arguments)
{
Service.Get().ToggleLogWindow();
diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs
index 1ee248b17..caf014885 100644
--- a/Dalamud/Interface/Internal/DalamudIme.cs
+++ b/Dalamud/Interface/Internal/DalamudIme.cs
@@ -1,8 +1,11 @@
+// #define IMEDEBUG
+
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@@ -17,6 +20,10 @@ using Dalamud.Interface.Utility;
using ImGuiNET;
+#if IMEDEBUG
+using Serilog;
+#endif
+
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
@@ -26,12 +33,21 @@ namespace Dalamud.Interface.Internal;
///
/// This class handles CJK IME.
///
-[ServiceManager.BlockingEarlyLoadedService]
+[ServiceManager.EarlyLoadedService]
internal sealed unsafe class DalamudIme : IDisposable, IServiceType
{
private const int CImGuiStbTextCreateUndoOffset = 0xB57A0;
private const int CImGuiStbTextUndoOffset = 0xB59C0;
+ private const int ImePageSize = 9;
+
+ private static readonly Dictionary WmNames =
+ typeof(WM).GetFields(BindingFlags.Public | BindingFlags.Static)
+ .Where(x => x.IsLiteral && !x.IsInitOnly && x.FieldType == typeof(int))
+ .Select(x => ((int)x.GetRawConstantValue()!, x.Name))
+ .DistinctBy(x => x.Item1)
+ .ToDictionary(x => x.Item1, x => x.Name);
+
private static readonly UnicodeRange[] HanRange =
{
UnicodeRanges.CjkRadicalsSupplement,
@@ -57,10 +73,46 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
private static readonly delegate* unmanaged StbTextUndo;
+ [ServiceManager.ServiceDependency]
+ private readonly WndProcHookManager wndProcHookManager = Service.Get();
+
+ private readonly InterfaceManager interfaceManager;
+
private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate;
+ /// The candidates.
+ private readonly List candidateStrings = new();
+
+ /// The selected imm component.
+ private string compositionString = string.Empty;
+
+ /// The cursor position in screen coordinates.
+ private Vector2 cursorScreenPos;
+
+ /// The associated viewport.
+ private ImGuiViewportPtr associatedViewport;
+
+ /// The index of the first imm candidate in relation to the full list.
+ private CANDIDATELIST immCandNative;
+
+ /// The partial conversion from-range.
+ private int partialConversionFrom;
+
+ /// The partial conversion to-range.
+ private int partialConversionTo;
+
+ /// The cursor offset in the composition string.
+ private int compositionCursorOffset;
+
+ /// The input mode icon from .
+ private char inputModeIcon;
+
+ /// Undo range for modifying the buffer while composition is in progress.
private (int Start, int End, int Cursor)? temporaryUndoSelection;
+ private bool updateInputLanguage = true;
+ private bool updateImeStatusAgain;
+
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1003:Symbols should be spaced correctly", Justification = ".")]
static DalamudIme()
{
@@ -87,7 +139,17 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
}
[ServiceManager.ServiceConstructor]
- private DalamudIme() => this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData;
+ private DalamudIme(InterfaceManager.InterfaceManagerWithScene imws)
+ {
+ Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?");
+
+ this.interfaceManager = imws.Manager;
+ this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData;
+
+ ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate);
+ this.interfaceManager.Draw += this.Draw;
+ this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc;
+ }
///
/// Finalizes an instance of the class.
@@ -109,7 +171,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
///
/// Gets a value indicating whether to display the cursor in input text. This also deals with blinking.
///
- internal static bool ShowCursorInInputText
+ private static bool ShowCursorInInputText
{
get
{
@@ -126,63 +188,21 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
}
}
- ///
- /// Gets the cursor position, in screen coordinates.
- ///
- internal Vector2 CursorPos { get; private set; }
-
- ///
- /// Gets the associated viewport.
- ///
- internal ImGuiViewportPtr AssociatedViewport { get; private set; }
-
- ///
- /// Gets the index of the first imm candidate in relation to the full list.
- ///
- internal CANDIDATELIST ImmCandNative { get; private set; }
-
- ///
- /// Gets the imm candidates.
- ///
- internal List ImmCand { get; private set; } = new();
-
- ///
- /// Gets the selected imm component.
- ///
- internal string ImmComp { get; private set; } = string.Empty;
-
- ///
- /// Gets the partial conversion from-range.
- ///
- internal int PartialConversionFrom { get; private set; }
-
- ///
- /// Gets the partial conversion to-range.
- ///
- internal int PartialConversionTo { get; private set; }
-
- ///
- /// Gets the cursor offset in the composition string.
- ///
- internal int CompositionCursorOffset { get; private set; }
-
- ///
- /// Gets a value indicating whether to display partial conversion status.
- ///
- internal bool ShowPartialConversion => this.PartialConversionFrom != 0 ||
- this.PartialConversionTo != this.ImmComp.Length;
-
- ///
- /// Gets the input mode icon from .
- ///
- internal char InputModeIcon { get; private set; }
-
private static ImGuiInputTextState* TextState =>
(ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextOffsets.TextStateOffset);
+ /// Gets a value indicating whether to display partial conversion status.
+ private bool ShowPartialConversion => this.partialConversionFrom != 0 ||
+ this.partialConversionTo != this.compositionString.Length;
+
+ /// Gets a value indicating whether to draw.
+ private bool ShouldDraw =>
+ this.candidateStrings.Count != 0 || this.ShowPartialConversion || this.inputModeIcon != default;
+
///
public void Dispose()
{
+ this.interfaceManager.Draw -= this.Draw;
this.ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
@@ -195,13 +215,13 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
{
foreach (var chr in str)
{
- if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length))
+ if (!this.EncounteredHan)
{
- if (Service.Get()
- ?.GetFdtReader(GameFontFamilyAndSize.Axis12)
- .FindGlyph(chr) is null)
+ if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length))
{
- if (!this.EncounteredHan)
+ if (Service.Get()
+ ?.GetFdtReader(GameFontFamilyAndSize.Axis12)
+ .FindGlyph(chr) is null)
{
this.EncounteredHan = true;
Service.Get().RebuildFonts();
@@ -209,9 +229,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
}
}
- if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length))
+ if (!this.EncounteredHangul)
{
- if (!this.EncounteredHangul)
+ if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length))
{
this.EncounteredHangul = true;
Service.Get().RebuildFonts();
@@ -220,112 +240,6 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
}
}
- ///
- /// Processes window messages.
- ///
- /// The arguments.
- public void ProcessImeMessage(WndProcEventArgs args)
- {
- if (!ImGuiHelpers.IsImGuiInitialized)
- return;
-
- // Are we not the target of text input?
- if (!ImGui.GetIO().WantTextInput)
- return;
-
- var hImc = ImmGetContext(args.Hwnd);
- if (hImc == nint.Zero)
- return;
-
- try
- {
- var invalidTarget = TextState->Id == 0 || (TextState->Flags & ImGuiInputTextFlags.ReadOnly) != 0;
-
- switch (args.Message)
- {
- case WM.WM_IME_NOTIFY
- when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE
- or IMN.IMN_CHANGECANDIDATE:
- this.UpdateImeWindowStatus(hImc);
- args.SuppressWithValue(0);
- break;
-
- case WM.WM_IME_STARTCOMPOSITION:
- args.SuppressWithValue(0);
- break;
-
- case WM.WM_IME_COMPOSITION:
- if (invalidTarget)
- ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0);
- else
- this.ReplaceCompositionString(hImc, (uint)args.LParam);
-
- // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.ImmComp}");
- args.SuppressWithValue(0);
- break;
-
- case WM.WM_IME_ENDCOMPOSITION:
- // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}");
- args.SuppressWithValue(0);
- break;
-
- case WM.WM_IME_CONTROL:
- // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}");
- args.SuppressWithValue(0);
- break;
-
- case WM.WM_IME_REQUEST:
- // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}");
- args.SuppressWithValue(0);
- break;
-
- case WM.WM_IME_SETCONTEXT:
- // Hide candidate and composition windows.
- args.LParam = (LPARAM)((nint)args.LParam & ~(ISC_SHOWUICOMPOSITIONWINDOW | 0xF));
-
- // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}");
- args.SuppressWithDefault();
- break;
-
- case WM.WM_IME_NOTIFY:
- // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}");
- break;
-
- case WM.WM_KEYDOWN when (int)args.WParam is
- VK.VK_TAB
- or VK.VK_PRIOR
- or VK.VK_NEXT
- or VK.VK_END
- or VK.VK_HOME
- or VK.VK_LEFT
- or VK.VK_UP
- or VK.VK_RIGHT
- or VK.VK_DOWN
- or VK.VK_RETURN:
- if (this.ImmCand.Count != 0)
- {
- this.ClearState(hImc);
- args.WParam = VK.VK_PROCESSKEY;
- }
-
- break;
-
- case WM.WM_LBUTTONDOWN:
- case WM.WM_RBUTTONDOWN:
- case WM.WM_MBUTTONDOWN:
- case WM.WM_XBUTTONDOWN:
- ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
- break;
- }
-
- this.UpdateInputLanguage(hImc);
- }
- finally
- {
- ImmReleaseContext(args.Hwnd, hImc);
- }
- }
-
private static string ImmGetCompositionString(HIMC hImc, uint comp)
{
var numBytes = ImmGetCompositionStringW(hImc, comp, null, 0);
@@ -343,6 +257,187 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
ImGui.GetIO().SetPlatformImeDataFn = nint.Zero;
}
+ private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args)
+ {
+ if (!ImGuiHelpers.IsImGuiInitialized)
+ {
+ this.updateInputLanguage = true;
+ return;
+ }
+
+ // Are we not the target of text input?
+ if (!ImGui.GetIO().WantTextInput)
+ {
+ this.updateInputLanguage = true;
+ return;
+ }
+
+ var hImc = ImmGetContext(args.Hwnd);
+ if (hImc == nint.Zero)
+ {
+ this.updateInputLanguage = true;
+ return;
+ }
+
+ try
+ {
+ var invalidTarget = TextState->Id == 0 || (TextState->Flags & ImGuiInputTextFlags.ReadOnly) != 0;
+
+#if IMEDEBUG
+ switch (args.Message)
+ {
+ case WM.WM_IME_NOTIFY:
+ Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({ImeDebug.ImnName((int)args.WParam)}, 0x{args.LParam:X})");
+ break;
+ case WM.WM_IME_CONTROL:
+ Log.Verbose(
+ $"{nameof(WM.WM_IME_CONTROL)}({ImeDebug.ImcName((int)args.WParam)}, 0x{args.LParam:X})");
+ break;
+ case WM.WM_IME_REQUEST:
+ Log.Verbose(
+ $"{nameof(WM.WM_IME_REQUEST)}({ImeDebug.ImrName((int)args.WParam)}, 0x{args.LParam:X})");
+ break;
+ case WM.WM_IME_SELECT:
+ Log.Verbose($"{nameof(WM.WM_IME_SELECT)}({(int)args.WParam != 0}, 0x{args.LParam:X})");
+ break;
+ case WM.WM_IME_STARTCOMPOSITION:
+ Log.Verbose($"{nameof(WM.WM_IME_STARTCOMPOSITION)}()");
+ break;
+ case WM.WM_IME_COMPOSITION:
+ Log.Verbose(
+ $"{nameof(WM.WM_IME_COMPOSITION)}({(char)args.WParam}, {ImeDebug.GcsName((int)args.LParam)})");
+ break;
+ case WM.WM_IME_COMPOSITIONFULL:
+ Log.Verbose($"{nameof(WM.WM_IME_COMPOSITIONFULL)}()");
+ break;
+ case WM.WM_IME_ENDCOMPOSITION:
+ Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}()");
+ break;
+ case WM.WM_IME_CHAR:
+ Log.Verbose($"{nameof(WM.WM_IME_CHAR)}({(char)args.WParam}, 0x{args.LParam:X})");
+ break;
+ case WM.WM_IME_KEYDOWN:
+ Log.Verbose($"{nameof(WM.WM_IME_KEYDOWN)}({(char)args.WParam}, 0x{args.LParam:X})");
+ break;
+ case WM.WM_IME_KEYUP:
+ Log.Verbose($"{nameof(WM.WM_IME_KEYUP)}({(char)args.WParam}, 0x{args.LParam:X})");
+ break;
+ case WM.WM_IME_SETCONTEXT:
+ Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(int)args.WParam != 0}, 0x{args.LParam:X})");
+ break;
+ }
+#endif
+ if (this.updateInputLanguage
+ || (args.Message == WM.WM_IME_NOTIFY
+ && (int)args.WParam
+ is IMN.IMN_SETCONVERSIONMODE
+ or IMN.IMN_OPENSTATUSWINDOW
+ or IMN.IMN_CLOSESTATUSWINDOW))
+ {
+ this.UpdateInputLanguage(hImc);
+ this.updateInputLanguage = false;
+ }
+
+ if (this.updateImeStatusAgain)
+ {
+ this.ReplaceCompositionString(hImc, false);
+ this.UpdateCandidates(hImc);
+ this.updateImeStatusAgain = false;
+ }
+
+ switch (args.Message)
+ {
+ case WM.WM_IME_NOTIFY
+ when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE
+ or IMN.IMN_CHANGECANDIDATE:
+ this.UpdateCandidates(hImc);
+ this.updateImeStatusAgain = true;
+ args.SuppressWithValue(0);
+ break;
+
+ case WM.WM_IME_STARTCOMPOSITION:
+ this.updateImeStatusAgain = true;
+ args.SuppressWithValue(0);
+ break;
+
+ case WM.WM_IME_COMPOSITION:
+ if (invalidTarget)
+ ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0);
+ else
+ this.ReplaceCompositionString(hImc, ((int)args.LParam & GCS.GCS_RESULTSTR) != 0);
+
+ this.updateImeStatusAgain = true;
+ args.SuppressWithValue(0);
+ break;
+
+ case WM.WM_IME_ENDCOMPOSITION:
+ this.ClearState(hImc, false);
+ this.updateImeStatusAgain = true;
+ args.SuppressWithValue(0);
+ break;
+
+ case WM.WM_IME_CHAR:
+ case WM.WM_IME_KEYDOWN:
+ case WM.WM_IME_KEYUP:
+ case WM.WM_IME_CONTROL:
+ case WM.WM_IME_REQUEST:
+ this.updateImeStatusAgain = true;
+ args.SuppressWithValue(0);
+ break;
+
+ case WM.WM_IME_SETCONTEXT:
+ // Hide candidate and composition windows.
+ args.LParam = (LPARAM)((nint)args.LParam & ~(ISC_SHOWUICOMPOSITIONWINDOW | 0xF));
+
+ this.updateImeStatusAgain = true;
+ args.SuppressWithDefault();
+ break;
+
+ case WM.WM_IME_NOTIFY:
+ case WM.WM_IME_COMPOSITIONFULL:
+ case WM.WM_IME_SELECT:
+ this.updateImeStatusAgain = true;
+ break;
+
+ case WM.WM_KEYDOWN when (int)args.WParam is
+ VK.VK_TAB
+ or VK.VK_PRIOR
+ or VK.VK_NEXT
+ or VK.VK_END
+ or VK.VK_HOME
+ or VK.VK_LEFT
+ or VK.VK_UP
+ or VK.VK_RIGHT
+ or VK.VK_DOWN
+ or VK.VK_RETURN:
+ if (this.candidateStrings.Count != 0)
+ {
+ this.ClearState(hImc);
+ args.WParam = VK.VK_PROCESSKEY;
+ }
+
+ this.UpdateCandidates(hImc);
+ break;
+
+ case WM.WM_KEYDOWN when (int)args.WParam is VK.VK_ESCAPE && this.candidateStrings.Count != 0:
+ this.ClearState(hImc);
+ args.SuppressWithDefault();
+ break;
+
+ case WM.WM_LBUTTONDOWN:
+ case WM.WM_RBUTTONDOWN:
+ case WM.WM_MBUTTONDOWN:
+ case WM.WM_XBUTTONDOWN:
+ ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
+ break;
+ }
+ }
+ finally
+ {
+ ImmReleaseContext(args.Hwnd, hImc);
+ }
+ }
+
private void UpdateInputLanguage(HIMC hImc)
{
uint conv, sent;
@@ -350,8 +445,6 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
var lang = GetKeyboardLayout(0);
var open = ImmGetOpenStatus(hImc) != false;
- // Log.Verbose($"{nameof(this.UpdateInputLanguage)}: conv={conv:X} sent={sent:X} open={open} lang={lang:X}");
-
var native = (conv & 1) != 0;
var katakana = (conv & 2) != 0;
var fullwidth = (conv & 8) != 0;
@@ -359,50 +452,51 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
{
case LANG.LANG_KOREAN:
if (native)
- this.InputModeIcon = (char)SeIconChar.ImeKoreanHangul;
+ this.inputModeIcon = (char)SeIconChar.ImeKoreanHangul;
else if (fullwidth)
- this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric;
+ this.inputModeIcon = (char)SeIconChar.ImeAlphanumeric;
else
- this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth;
+ this.inputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth;
break;
case LANG.LANG_JAPANESE:
// wtf
// see the function called from: 48 8b 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 8b d8 e9 ?? 00 00 0
if (open && native && katakana && fullwidth)
- this.InputModeIcon = (char)SeIconChar.ImeKatakana;
+ this.inputModeIcon = (char)SeIconChar.ImeKatakana;
else if (open && native && katakana)
- this.InputModeIcon = (char)SeIconChar.ImeKatakanaHalfWidth;
+ this.inputModeIcon = (char)SeIconChar.ImeKatakanaHalfWidth;
else if (open && native)
- this.InputModeIcon = (char)SeIconChar.ImeHiragana;
+ this.inputModeIcon = (char)SeIconChar.ImeHiragana;
else if (open && fullwidth)
- this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric;
+ this.inputModeIcon = (char)SeIconChar.ImeAlphanumeric;
else
- this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth;
+ this.inputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth;
break;
case LANG.LANG_CHINESE:
if (native)
- this.InputModeIcon = (char)SeIconChar.ImeChineseHan;
+ this.inputModeIcon = (char)SeIconChar.ImeChineseHan;
else
- this.InputModeIcon = (char)SeIconChar.ImeChineseLatin;
+ this.inputModeIcon = (char)SeIconChar.ImeChineseLatin;
break;
default:
- this.InputModeIcon = default;
+ this.inputModeIcon = default;
break;
}
-
- this.UpdateImeWindowStatus(hImc);
}
- private void ReplaceCompositionString(HIMC hImc, uint comp)
+ private void ReplaceCompositionString(HIMC hImc, bool finalCommit)
{
- var finalCommit = (comp & GCS.GCS_RESULTSTR) != 0;
var newString = finalCommit
? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR)
: ImmGetCompositionString(hImc, GCS.GCS_COMPSTR);
+#if IMEDEBUG
+ Log.Verbose($"{nameof(this.ReplaceCompositionString)}({newString})");
+#endif
+
this.ReflectCharacterEncounters(newString);
if (this.temporaryUndoSelection is not null)
@@ -421,18 +515,18 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
if (finalCommit)
{
- this.ClearState(hImc);
- return;
+ this.ClearState(hImc, false);
+ newString = string.Empty;
}
- this.ImmComp = newString;
- this.CompositionCursorOffset = ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0);
+ this.compositionString = newString;
+ this.compositionCursorOffset = ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0);
- if ((comp & GCS.GCS_COMPATTR) != 0)
+ var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0);
+ if (attrLength > 0)
{
- var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0);
var attrPtr = stackalloc byte[attrLength];
- var attr = new Span(attrPtr, Math.Min(this.ImmComp.Length, attrLength));
+ var attr = new Span(attrPtr, Math.Min(this.compositionString.Length, attrLength));
_ = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, attrPtr, (uint)attrLength);
var l = 0;
while (l < attr.Length && attr[l] is not ATTR_TARGET_CONVERTED and not ATTR_TARGET_NOTCONVERTED)
@@ -442,37 +536,41 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
while (r < attr.Length && attr[r] is ATTR_TARGET_CONVERTED or ATTR_TARGET_NOTCONVERTED)
r++;
- if (r == 0 || l == this.ImmComp.Length)
- (l, r) = (0, this.ImmComp.Length);
+ if (r == 0 || l == this.compositionString.Length)
+ (l, r) = (0, this.compositionString.Length);
- (this.PartialConversionFrom, this.PartialConversionTo) = (l, r);
+ (this.partialConversionFrom, this.partialConversionTo) = (l, r);
}
else
{
- this.PartialConversionFrom = 0;
- this.PartialConversionTo = this.ImmComp.Length;
+ this.partialConversionFrom = 0;
+ this.partialConversionTo = this.compositionString.Length;
}
- this.UpdateImeWindowStatus(hImc);
+ this.UpdateCandidates(hImc);
}
- private void ClearState(HIMC hImc)
+ private void ClearState(HIMC hImc, bool invokeCancel = true)
{
- this.ImmComp = string.Empty;
- this.PartialConversionFrom = this.PartialConversionTo = 0;
- this.CompositionCursorOffset = 0;
+ this.compositionString = string.Empty;
+ this.partialConversionFrom = this.partialConversionTo = 0;
+ this.compositionCursorOffset = 0;
this.temporaryUndoSelection = null;
TextState->Stb.SelectStart = TextState->Stb.Cursor = TextState->Stb.SelectEnd;
- ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0);
- this.UpdateImeWindowStatus(default);
+ this.candidateStrings.Clear();
+ this.immCandNative = default;
+ if (invokeCancel)
+ ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0);
- // Log.Information($"{nameof(this.ClearState)}");
+#if IMEDEBUG
+ Log.Information($"{nameof(this.ClearState)}({invokeCancel})");
+#endif
}
- private void LoadCand(HIMC hImc)
+ private void UpdateCandidates(HIMC hImc)
{
- this.ImmCand.Clear();
- this.ImmCandNative = default;
+ this.candidateStrings.Clear();
+ this.immCandNative = default;
if (hImc == default)
return;
@@ -486,7 +584,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
return;
ref var candlist = ref *(CANDIDATELIST*)pStorage;
- this.ImmCandNative = candlist;
+ this.immCandNative = candlist;
if (candlist.dwPageSize == 0 || candlist.dwCount == 0)
return;
@@ -495,39 +593,250 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
(int)candlist.dwPageStart,
(int)Math.Min(candlist.dwCount - candlist.dwPageStart, candlist.dwPageSize)))
{
- this.ImmCand.Add(new((char*)(pStorage + candlist.dwOffset[i])));
- this.ReflectCharacterEncounters(this.ImmCand[^1]);
+ this.candidateStrings.Add(new((char*)(pStorage + candlist.dwOffset[i])));
+ this.ReflectCharacterEncounters(this.candidateStrings[^1]);
}
}
- private void UpdateImeWindowStatus(HIMC hImc)
- {
- if (Service.GetNullable() is not { } di)
- return;
-
- this.LoadCand(hImc);
- if (this.ImmCand.Count != 0 || this.ShowPartialConversion || this.InputModeIcon != default)
- di.OpenImeWindow();
- else
- di.CloseImeWindow();
- }
-
private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data)
{
- this.CursorPos = data.InputPos;
- this.AssociatedViewport = data.WantVisible ? viewport : default;
+ this.cursorScreenPos = data.InputPos;
+ this.associatedViewport = data.WantVisible ? viewport : default;
}
- [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui context initialization.")]
- private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene)
+ private void Draw()
{
- if (!ImGuiHelpers.IsImGuiInitialized)
+ if (!this.ShouldDraw)
+ return;
+
+ if (Service.GetNullable() is not { } ime)
+ return;
+
+ var viewport = ime.associatedViewport;
+ if (viewport.NativePtr is null)
+ return;
+
+ var drawCand = ime.candidateStrings.Count != 0;
+ var drawConv = drawCand || ime.ShowPartialConversion;
+ var drawIme = ime.inputModeIcon != 0;
+ var imeIconFont = InterfaceManager.DefaultFont;
+
+ var pad = ImGui.GetStyle().WindowPadding;
+ var candTextSize = ImGui.CalcTextSize(ime.compositionString == string.Empty ? " " : ime.compositionString);
+
+ var native = ime.immCandNative;
+ var totalIndex = native.dwSelection + 1;
+ var totalSize = native.dwCount;
+
+ var pageStart = native.dwPageStart;
+ var pageIndex = (pageStart / ImePageSize) + 1;
+ var pageCount = (totalSize / ImePageSize) + 1;
+ var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})";
+
+ // Calc the window size.
+ var maxTextWidth = 0f;
+ for (var i = 0; i < ime.candidateStrings.Count; i++)
{
- throw new InvalidOperationException(
- $"Expected {nameof(InterfaceManager.InterfaceManagerWithScene)} to have initialized ImGui.");
+ var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.candidateStrings[i]}");
+ maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X;
}
- ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate);
+ maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X;
+ maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.compositionString).X
+ ? maxTextWidth
+ : ImGui.CalcTextSize(ime.compositionString).X;
+
+ var numEntries = (drawCand ? ime.candidateStrings.Count + 1 : 0) + 1 + (drawIme ? 1 : 0);
+ var spaceY = ImGui.GetStyle().ItemSpacing.Y;
+ var imeWindowHeight = (spaceY * (numEntries - 1)) + (candTextSize.Y * numEntries);
+ var windowSize = new Vector2(maxTextWidth, imeWindowHeight) + (pad * 2);
+
+ // 1. Figure out the expanding direction.
+ var expandUpward = ime.cursorScreenPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y;
+ var windowPos = ime.cursorScreenPos - pad;
+ if (expandUpward)
+ {
+ windowPos.Y -= windowSize.Y - candTextSize.Y - (pad.Y * 2);
+ if (drawIme)
+ windowPos.Y += candTextSize.Y + spaceY;
+ }
+ else
+ {
+ if (drawIme)
+ windowPos.Y -= candTextSize.Y + spaceY;
+ }
+
+ // 2. Contain within the viewport. Do not use clamp, as the target window might be too small.
+ if (windowPos.X < viewport.WorkPos.X)
+ windowPos.X = viewport.WorkPos.X;
+ else if (windowPos.X + windowSize.X > viewport.WorkPos.X + viewport.WorkSize.X)
+ windowPos.X = (viewport.WorkPos.X + viewport.WorkSize.X) - windowSize.X;
+ if (windowPos.Y < viewport.WorkPos.Y)
+ windowPos.Y = viewport.WorkPos.Y;
+ else if (windowPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y)
+ windowPos.Y = (viewport.WorkPos.Y + viewport.WorkSize.Y) - windowSize.Y;
+
+ var cursor = windowPos + pad;
+
+ // Draw the ime window.
+ var drawList = ImGui.GetForegroundDrawList(viewport);
+
+ // Draw the background rect for candidates.
+ if (drawCand)
+ {
+ Vector2 candRectLt, candRectRb;
+ if (!expandUpward)
+ {
+ candRectLt = windowPos + candTextSize with { X = 0 } + pad with { X = 0 };
+ candRectRb = windowPos + windowSize;
+ if (drawIme)
+ candRectLt.Y += spaceY + candTextSize.Y;
+ }
+ else
+ {
+ candRectLt = windowPos;
+ candRectRb = windowPos + (windowSize - candTextSize with { X = 0 } - pad with { X = 0 });
+ if (drawIme)
+ candRectRb.Y -= spaceY + candTextSize.Y;
+ }
+
+ drawList.AddRectFilled(
+ candRectLt,
+ candRectRb,
+ ImGui.GetColorU32(ImGuiCol.WindowBg),
+ ImGui.GetStyle().WindowRounding);
+ }
+
+ if (!expandUpward && drawIme)
+ {
+ for (var dx = -2; dx <= 2; dx++)
+ {
+ for (var dy = -2; dy <= 2; dy++)
+ {
+ if (dx != 0 || dy != 0)
+ {
+ imeIconFont.RenderChar(
+ drawList,
+ imeIconFont.FontSize,
+ cursor + new Vector2(dx, dy),
+ ImGui.GetColorU32(ImGuiCol.WindowBg),
+ ime.inputModeIcon);
+ }
+ }
+ }
+
+ imeIconFont.RenderChar(
+ drawList,
+ imeIconFont.FontSize,
+ cursor,
+ ImGui.GetColorU32(ImGuiCol.Text),
+ ime.inputModeIcon);
+ cursor.Y += candTextSize.Y + spaceY;
+ }
+
+ if (!expandUpward && drawConv)
+ {
+ DrawTextBeingConverted();
+ cursor.Y += candTextSize.Y + spaceY;
+
+ // Add a separator.
+ drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator));
+ }
+
+ if (drawCand)
+ {
+ // Add the candidate words.
+ for (var i = 0; i < ime.candidateStrings.Count; i++)
+ {
+ var selected = i == (native.dwSelection % ImePageSize);
+ var color = ImGui.GetColorU32(ImGuiCol.Text);
+ if (selected)
+ color = ImGui.GetColorU32(ImGuiCol.NavHighlight);
+
+ drawList.AddText(cursor, color, $"{i + 1}. {ime.candidateStrings[i]}");
+ cursor.Y += candTextSize.Y + spaceY;
+ }
+
+ // Add a separator
+ drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator));
+
+ // Add the pages infomation.
+ drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), pageInfo);
+ cursor.Y += candTextSize.Y + spaceY;
+ }
+
+ if (expandUpward && drawConv)
+ {
+ // Add a separator.
+ drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator));
+
+ DrawTextBeingConverted();
+ cursor.Y += candTextSize.Y + spaceY;
+ }
+
+ if (expandUpward && drawIme)
+ {
+ for (var dx = -2; dx <= 2; dx++)
+ {
+ for (var dy = -2; dy <= 2; dy++)
+ {
+ if (dx != 0 || dy != 0)
+ {
+ imeIconFont.RenderChar(
+ drawList,
+ imeIconFont.FontSize,
+ cursor + new Vector2(dx, dy),
+ ImGui.GetColorU32(ImGuiCol.WindowBg),
+ ime.inputModeIcon);
+ }
+ }
+ }
+
+ imeIconFont.RenderChar(
+ drawList,
+ imeIconFont.FontSize,
+ cursor,
+ ImGui.GetColorU32(ImGuiCol.Text),
+ ime.inputModeIcon);
+ }
+
+ return;
+
+ void DrawTextBeingConverted()
+ {
+ // Draw the text background.
+ drawList.AddRectFilled(
+ cursor - (pad / 2),
+ cursor + candTextSize + (pad / 2),
+ ImGui.GetColorU32(ImGuiCol.WindowBg));
+
+ // If only a part of the full text is marked for conversion, then draw background for the part being edited.
+ if (ime.partialConversionFrom != 0 || ime.partialConversionTo != ime.compositionString.Length)
+ {
+ var part1 = ime.compositionString[..ime.partialConversionFrom];
+ var part2 = ime.compositionString[..ime.partialConversionTo];
+ var size1 = ImGui.CalcTextSize(part1);
+ var size2 = ImGui.CalcTextSize(part2);
+ drawList.AddRectFilled(
+ cursor + size1 with { Y = 0 },
+ cursor + size2,
+ ImGui.GetColorU32(ImGuiCol.TextSelectedBg));
+ }
+
+ // Add the text being converted.
+ drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.compositionString);
+
+ // Draw the caret inside the composition string.
+ if (DalamudIme.ShowCursorInInputText)
+ {
+ var partBeforeCaret = ime.compositionString[..ime.compositionCursorOffset];
+ var sizeBeforeCaret = ImGui.CalcTextSize(partBeforeCaret);
+ drawList.AddLine(
+ cursor + sizeBeforeCaret with { Y = 0 },
+ cursor + sizeBeforeCaret,
+ ImGui.GetColorU32(ImGuiCol.Text));
+ }
+ }
}
///
@@ -706,4 +1015,71 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
return true;
}
}
+
+#if IMEDEBUG
+ private static class ImeDebug
+ {
+ private static readonly (int Value, string Name)[] GcsFields =
+ {
+ (GCS.GCS_COMPREADSTR, nameof(GCS.GCS_COMPREADSTR)),
+ (GCS.GCS_COMPREADATTR, nameof(GCS.GCS_COMPREADATTR)),
+ (GCS.GCS_COMPREADCLAUSE, nameof(GCS.GCS_COMPREADCLAUSE)),
+ (GCS.GCS_COMPSTR, nameof(GCS.GCS_COMPSTR)),
+ (GCS.GCS_COMPATTR, nameof(GCS.GCS_COMPATTR)),
+ (GCS.GCS_COMPCLAUSE, nameof(GCS.GCS_COMPCLAUSE)),
+ (GCS.GCS_CURSORPOS, nameof(GCS.GCS_CURSORPOS)),
+ (GCS.GCS_DELTASTART, nameof(GCS.GCS_DELTASTART)),
+ (GCS.GCS_RESULTREADSTR, nameof(GCS.GCS_RESULTREADSTR)),
+ (GCS.GCS_RESULTREADCLAUSE, nameof(GCS.GCS_RESULTREADCLAUSE)),
+ (GCS.GCS_RESULTSTR, nameof(GCS.GCS_RESULTSTR)),
+ (GCS.GCS_RESULTCLAUSE, nameof(GCS.GCS_RESULTCLAUSE)),
+ };
+
+ private static readonly IReadOnlyDictionary ImnFields =
+ typeof(IMN)
+ .GetFields(BindingFlags.Static | BindingFlags.Public)
+ .Where(x => x.IsLiteral)
+ .ToDictionary(x => (int)x.GetRawConstantValue()!, x => x.Name);
+
+ public static string GcsName(int val)
+ {
+ var sb = new StringBuilder();
+ foreach (var (value, name) in GcsFields)
+ {
+ if ((val & value) != 0)
+ {
+ if (sb.Length != 0)
+ sb.Append(" | ");
+ sb.Append(name);
+ val &= ~value;
+ }
+ }
+
+ if (val != 0)
+ {
+ if (sb.Length != 0)
+ sb.Append(" | ");
+ sb.Append($"0x{val:X}");
+ }
+
+ return sb.ToString();
+ }
+
+ public static string ImcName(int val) => ImnFields.TryGetValue(val, out var name) ? name : $"0x{val:X}";
+
+ public static string ImnName(int val) => ImnFields.TryGetValue(val, out var name) ? name : $"0x{val:X}";
+
+ public static string ImrName(int val) => val switch
+ {
+ IMR_CANDIDATEWINDOW => nameof(IMR_CANDIDATEWINDOW),
+ IMR_COMPOSITIONFONT => nameof(IMR_COMPOSITIONFONT),
+ IMR_COMPOSITIONWINDOW => nameof(IMR_COMPOSITIONWINDOW),
+ IMR_CONFIRMRECONVERTSTRING => nameof(IMR_CONFIRMRECONVERTSTRING),
+ IMR_DOCUMENTFEED => nameof(IMR_DOCUMENTFEED),
+ IMR_QUERYCHARPOSITION => nameof(IMR_QUERYCHARPOSITION),
+ IMR_RECONVERTSTRING => nameof(IMR_RECONVERTSTRING),
+ _ => $"0x{val:X}",
+ };
+ }
+#endif
}
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index 00bef19af..1a07cd6ae 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -61,7 +61,6 @@ internal class DalamudInterface : IDisposable, IServiceType
private readonly ComponentDemoWindow componentDemoWindow;
private readonly DataWindow dataWindow;
private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow;
- private readonly DalamudImeWindow imeWindow;
private readonly ConsoleWindow consoleWindow;
private readonly PluginStatWindow pluginStatWindow;
private readonly PluginInstallerWindow pluginWindow;
@@ -114,7 +113,6 @@ internal class DalamudInterface : IDisposable, IServiceType
this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false };
this.dataWindow = new DataWindow() { IsOpen = false };
this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow() { IsOpen = false };
- this.imeWindow = new DalamudImeWindow() { IsOpen = false };
this.consoleWindow = new ConsoleWindow(configuration) { IsOpen = configuration.LogOpenAtStartup };
this.pluginStatWindow = new PluginStatWindow() { IsOpen = false };
this.pluginWindow = new PluginInstallerWindow(pluginImageCache, configuration) { IsOpen = false };
@@ -142,7 +140,6 @@ internal class DalamudInterface : IDisposable, IServiceType
this.WindowSystem.AddWindow(this.componentDemoWindow);
this.WindowSystem.AddWindow(this.dataWindow);
this.WindowSystem.AddWindow(this.gamepadModeNotifierWindow);
- this.WindowSystem.AddWindow(this.imeWindow);
this.WindowSystem.AddWindow(this.consoleWindow);
this.WindowSystem.AddWindow(this.pluginStatWindow);
this.WindowSystem.AddWindow(this.pluginWindow);
@@ -265,11 +262,6 @@ internal class DalamudInterface : IDisposable, IServiceType
///
public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true;
- ///
- /// Opens the .
- ///
- public void OpenImeWindow() => this.imeWindow.IsOpen = true;
-
///
/// Opens the .
///
@@ -365,11 +357,6 @@ internal class DalamudInterface : IDisposable, IServiceType
#region Close
- ///
- /// Closes the .
- ///
- public void CloseImeWindow() => this.imeWindow.IsOpen = false;
-
///
/// Closes the .
///
@@ -417,11 +404,6 @@ internal class DalamudInterface : IDisposable, IServiceType
///
public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle();
- ///
- /// Toggles the .
- ///
- public void ToggleImeWindow() => this.imeWindow.Toggle();
-
///
/// Toggles the .
///
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 3db799be0..126097ed3 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -67,9 +67,6 @@ internal class InterfaceManager : IDisposable, IServiceType
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service.Get();
-
- [ServiceManager.ServiceDependency]
- private readonly DalamudIme dalamudIme = Service.Get();
private readonly SwapChainVtableResolver address = new();
private readonly Hook setCursorHook;
@@ -627,8 +624,6 @@ internal class InterfaceManager : IDisposable, IServiceType
var r = this.scene?.ProcessWndProcW(args.Hwnd, (User32.WindowMessage)args.Message, args.WParam, args.LParam);
if (r is not null)
args.SuppressWithValue(r.Value);
-
- this.dalamudIme.ProcessImeMessage(args);
}
/*
diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
index f36d79222..1957ab720 100644
--- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
@@ -1,24 +1,28 @@
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Drawing;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
-using System.Threading;
using Dalamud.Configuration.Internal;
+using Dalamud.Game;
using Dalamud.Game.Command;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
+using Dalamud.Interface.Internal.Notifications;
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;
+
using ImGuiNET;
+
using Serilog;
using Serilog.Events;
@@ -31,39 +35,48 @@ internal class ConsoleWindow : Window, IDisposable
{
private const int LogLinesMinimum = 100;
private const int LogLinesMaximum = 1000000;
-
+
+ // 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 volatile int newRolledLines;
- private readonly object renderLock = new();
+ private readonly RollingList filteredLogEntries;
private readonly List history = new();
private readonly List pluginFilters = new();
+ private int newRolledLines;
+ private bool pendingRefilter;
+ private bool pendingClearLog;
+
private bool? lastCmdSuccess;
+ private ImGuiListClipperPtr clipperPtr;
private string commandText = string.Empty;
private string textFilter = string.Empty;
+ private string textHighlight = string.Empty;
private string selectedSource = "DalamudInternal";
private string pluginFilter = string.Empty;
+ private Regex? compiledLogFilter;
+ private Regex? compiledLogHighlight;
+ private Exception? exceptionLogFilter;
+ private Exception? exceptionLogHighlight;
+
private bool filterShowUncaughtExceptions;
private bool settingsPopupWasOpen;
private bool showFilterToolbar;
- private bool clearLog;
- private bool copyLog;
private bool copyMode;
private bool killGameArmed;
private bool autoScroll;
private int logLinesLimit;
private bool autoOpen;
- private bool regexError;
private int historyPos;
private int copyStart = -1;
- ///
- /// Initializes a new instance of the class.
- ///
+ /// Initializes a new instance of the class.
/// An instance of .
public ConsoleWindow(DalamudConfiguration configuration)
: base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)
@@ -72,6 +85,8 @@ internal class ConsoleWindow : Window, IDisposable
this.autoOpen = configuration.LogOpenAtStartup;
SerilogEventSink.Instance.LogLine += this.OnLogLine;
+ Service.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate);
+
this.Size = new Vector2(500, 400);
this.SizeCondition = ImGuiCond.FirstUseEver;
@@ -85,13 +100,17 @@ 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);
+ this.filteredLogEntries = new(limit);
configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved;
- }
- private RollingList FilteredLogEntries { get; set; }
+ unsafe
+ {
+ this.clipperPtr = new(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
+ }
+ }
///
public override void OnOpen()
@@ -100,58 +119,16 @@ internal class ConsoleWindow : Window, IDisposable
base.OnOpen();
}
- ///
- /// Dispose of managed and unmanaged resources.
- ///
+ ///
public void Dispose()
{
SerilogEventSink.Instance.LogLine -= this.OnLogLine;
Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
- }
+ if (Service.GetNullable() is { } framework)
+ framework.Update -= this.FrameworkOnUpdate;
- ///
- /// Clear the window of all log entries.
- ///
- public void Clear()
- {
- lock (this.renderLock)
- {
- this.logText.Clear();
- this.FilteredLogEntries.Clear();
- this.clearLog = false;
- }
- }
-
- ///
- /// Copies the entire log contents to clipboard.
- ///
- public void CopyLog()
- {
- ImGui.LogToClipboard();
- }
-
- ///
- /// Add a single log line to the display.
- ///
- /// The line to add.
- /// The Serilog event associated with this line.
- public void HandleLogLine(string line, LogEvent logEvent)
- {
- if (line.IndexOfAny(new[] { '\n', '\r' }) != -1)
- {
- var subLines = line.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
-
- this.AddAndFilter(subLines[0], logEvent, false);
-
- for (var i = 1; i < subLines.Length; i++)
- {
- this.AddAndFilter(subLines[i], logEvent, true);
- }
- }
- else
- {
- this.AddAndFilter(line, logEvent, false);
- }
+ this.clipperPtr.Destroy();
+ this.clipperPtr = default;
}
///
@@ -161,112 +138,126 @@ internal class ConsoleWindow : Window, IDisposable
this.DrawFilterToolbar();
- if (this.regexError)
+ if (this.exceptionLogFilter is not null)
{
- const string regexErrorString = "Regex Filter Error";
- ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 2.0f - ImGui.CalcTextSize(regexErrorString).X / 2.0f);
- ImGui.TextColored(ImGuiColors.DalamudRed, regexErrorString);
+ ImGui.TextColored(
+ ImGuiColors.DalamudRed,
+ $"Regex Filter Error: {this.exceptionLogFilter.GetType().Name}");
+ ImGui.TextUnformatted(this.exceptionLogFilter.Message);
+ }
+
+ if (this.exceptionLogHighlight is not null)
+ {
+ ImGui.TextColored(
+ ImGuiColors.DalamudRed,
+ $"Regex Highlight Error: {this.exceptionLogHighlight.GetType().Name}");
+ ImGui.TextUnformatted(this.exceptionLogHighlight.Message);
}
var sendButtonSize = ImGui.CalcTextSize("Send") +
((new Vector2(16, 0) + (ImGui.GetStyle().FramePadding * 2)) * ImGuiHelpers.GlobalScale);
var scrollingHeight = ImGui.GetContentRegionAvail().Y - sendButtonSize.Y;
- ImGui.BeginChild("scrolling", new Vector2(0, scrollingHeight), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
-
- if (this.clearLog) this.Clear();
-
- if (this.copyLog) this.CopyLog();
+ ImGui.BeginChild(
+ "scrolling",
+ new Vector2(0, scrollingHeight),
+ false,
+ ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
- ImGuiListClipperPtr clipper;
- unsafe
- {
- clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
- }
-
ImGui.PushFont(InterfaceManager.MonoFont);
var childPos = ImGui.GetWindowPos();
var childDrawList = ImGui.GetWindowDrawList();
var childSize = ImGui.GetWindowSize();
- var cursorDiv = ImGui.CalcTextSize("00:00:00.000 ").X;
- var cursorLogLevel = ImGui.CalcTextSize("00:00:00.000 | ").X;
- var dividerOffset = ImGui.CalcTextSize("00:00:00.000 | AAA ").X + (ImGui.CalcTextSize(" ").X / 2);
- var cursorLogLine = ImGui.CalcTextSize("00:00:00.000 | AAA | ").X;
+ var timestampWidth = ImGui.CalcTextSize("00:00:00.000").X;
+ var levelWidth = ImGui.CalcTextSize("AAA").X;
+ var separatorWidth = ImGui.CalcTextSize(" | ").X;
+ var cursorLogLevel = timestampWidth + separatorWidth;
+ var cursorLogLine = cursorLogLevel + levelWidth + separatorWidth;
var lastLinePosY = 0.0f;
var logLineHeight = 0.0f;
- lock (this.renderLock)
+ this.clipperPtr.Begin(this.filteredLogEntries.Count);
+ while (this.clipperPtr.Step())
{
- clipper.Begin(this.FilteredLogEntries.Count);
- while (clipper.Step())
+ for (var i = this.clipperPtr.DisplayStart; i < this.clipperPtr.DisplayEnd; i++)
{
- for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
+ var index = Math.Max(
+ i - this.newRolledLines,
+ 0); // Prevents flicker effect. Also workaround to avoid negative indexes.
+ var line = this.filteredLogEntries[index];
+
+ if (!line.IsMultiline)
+ ImGui.Separator();
+
+ if (line.SelectedForCopy)
{
- var index = Math.Max(i - this.newRolledLines, 0); // Prevents flicker effect. Also workaround to avoid negative indexes.
- var line = this.FilteredLogEntries[index];
+ ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGrey);
+ ImGui.PushStyleColor(ImGuiCol.HeaderActive, ImGuiColors.ParsedGrey);
+ ImGui.PushStyleColor(ImGuiCol.HeaderHovered, ImGuiColors.ParsedGrey);
+ }
+ else
+ {
+ ImGui.PushStyleColor(ImGuiCol.Header, GetColorForLogEventLevel(line.Level));
+ ImGui.PushStyleColor(ImGuiCol.HeaderActive, GetColorForLogEventLevel(line.Level));
+ ImGui.PushStyleColor(ImGuiCol.HeaderHovered, GetColorForLogEventLevel(line.Level));
+ }
- if (!line.IsMultiline && !this.copyLog)
- ImGui.Separator();
-
- if (line.SelectedForCopy)
- {
- ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGrey);
- ImGui.PushStyleColor(ImGuiCol.HeaderActive, ImGuiColors.ParsedGrey);
- ImGui.PushStyleColor(ImGuiCol.HeaderHovered, ImGuiColors.ParsedGrey);
- }
- else
- {
- ImGui.PushStyleColor(ImGuiCol.Header, this.GetColorForLogEventLevel(line.Level));
- ImGui.PushStyleColor(ImGuiCol.HeaderActive, this.GetColorForLogEventLevel(line.Level));
- ImGui.PushStyleColor(ImGuiCol.HeaderHovered, this.GetColorForLogEventLevel(line.Level));
- }
+ ImGui.Selectable(
+ "###console_null",
+ true,
+ ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns);
- ImGui.Selectable("###console_null", true, ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns);
+ // This must be after ImGui.Selectable, it uses ImGui.IsItem... functions
+ this.HandleCopyMode(i, line);
- // This must be after ImGui.Selectable, it uses ImGui.IsItem... functions
- this.HandleCopyMode(i, line);
-
+ ImGui.SameLine();
+
+ ImGui.PopStyleColor(3);
+
+ if (!line.IsMultiline)
+ {
+ ImGui.TextUnformatted(line.TimestampString);
ImGui.SameLine();
- ImGui.PopStyleColor(3);
-
- if (!line.IsMultiline)
- {
- ImGui.TextUnformatted(line.TimeStamp.ToString("HH:mm:ss.fff"));
- ImGui.SameLine();
- ImGui.SetCursorPosX(cursorDiv);
- ImGui.TextUnformatted("|");
- ImGui.SameLine();
- ImGui.SetCursorPosX(cursorLogLevel);
- ImGui.TextUnformatted(this.GetTextForLogEventLevel(line.Level));
- ImGui.SameLine();
- }
-
- ImGui.SetCursorPosX(cursorLogLine);
- ImGui.TextUnformatted(line.Line);
-
- var currentLinePosY = ImGui.GetCursorPosY();
- logLineHeight = currentLinePosY - lastLinePosY;
- lastLinePosY = currentLinePosY;
+ ImGui.SetCursorPosX(cursorLogLevel);
+ ImGui.TextUnformatted(GetTextForLogEventLevel(line.Level));
+ ImGui.SameLine();
}
- }
- clipper.End();
- clipper.Destroy();
+ ImGui.SetCursorPosX(cursorLogLine);
+ line.HighlightMatches ??= (this.compiledLogHighlight ?? this.compiledLogFilter)?.Matches(line.Line);
+ if (line.HighlightMatches is { } matches)
+ {
+ this.DrawHighlighted(
+ line.Line,
+ matches,
+ ImGui.GetColorU32(ImGuiCol.Text),
+ ImGui.GetColorU32(ImGuiColors.HealerGreen));
+ }
+ else
+ {
+ ImGui.TextUnformatted(line.Line);
+ }
+
+ var currentLinePosY = ImGui.GetCursorPosY();
+ logLineHeight = currentLinePosY - lastLinePosY;
+ lastLinePosY = currentLinePosY;
+ }
}
+ this.clipperPtr.End();
+
ImGui.PopFont();
ImGui.PopStyleVar();
- var newRolledLinesCount = Interlocked.Exchange(ref this.newRolledLines, 0);
if (!this.autoScroll || ImGui.GetScrollY() < ImGui.GetScrollMaxY())
{
- ImGui.SetScrollY(ImGui.GetScrollY() - (logLineHeight * newRolledLinesCount));
+ ImGui.SetScrollY(ImGui.GetScrollY() - (logLineHeight * this.newRolledLines));
}
if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
@@ -274,8 +265,19 @@ internal class ConsoleWindow : Window, IDisposable
ImGui.SetScrollHereY(1.0f);
}
- // Draw dividing line
- childDrawList.AddLine(new Vector2(childPos.X + dividerOffset, childPos.Y), new Vector2(childPos.X + dividerOffset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f);
+ // Draw dividing lines
+ var div1Offset = MathF.Round((timestampWidth + (separatorWidth / 2)) - ImGui.GetScrollX());
+ var div2Offset = MathF.Round((cursorLogLevel + levelWidth + (separatorWidth / 2)) - ImGui.GetScrollX());
+ childDrawList.AddLine(
+ new(childPos.X + div1Offset, childPos.Y),
+ new(childPos.X + div1Offset, childPos.Y + childSize.Y),
+ 0x4FFFFFFF,
+ 1.0f);
+ childDrawList.AddLine(
+ new(childPos.X + div2Offset, childPos.Y),
+ new(childPos.X + div2Offset, childPos.Y + childSize.Y),
+ 0x4FFFFFFF,
+ 1.0f);
ImGui.EndChild();
@@ -293,12 +295,20 @@ internal class ConsoleWindow : Window, IDisposable
}
}
- ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - sendButtonSize.X - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
+ ImGui.SetNextItemWidth(
+ ImGui.GetContentRegionAvail().X - sendButtonSize.X -
+ (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
var getFocus = false;
unsafe
{
- if (ImGui.InputText("##command_box", ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory, this.CommandInputCallback))
+ if (ImGui.InputText(
+ "##command_box",
+ ref this.commandText,
+ 255,
+ ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion |
+ ImGuiInputTextFlags.CallbackHistory,
+ this.CommandInputCallback))
{
this.ProcessCommand();
getFocus = true;
@@ -316,14 +326,62 @@ internal class ConsoleWindow : Window, IDisposable
{
this.ProcessCommand();
}
-
- this.copyLog = false;
}
-
+
+ private static string GetTextForLogEventLevel(LogEventLevel level) => level switch
+ {
+ LogEventLevel.Error => "ERR",
+ LogEventLevel.Verbose => "VRB",
+ LogEventLevel.Debug => "DBG",
+ LogEventLevel.Information => "INF",
+ LogEventLevel.Warning => "WRN",
+ LogEventLevel.Fatal => "FTL",
+ _ => "???",
+ };
+
+ private static uint GetColorForLogEventLevel(LogEventLevel level) => level switch
+ {
+ LogEventLevel.Error => 0x800000EE,
+ LogEventLevel.Verbose => 0x00000000,
+ LogEventLevel.Debug => 0x00000000,
+ LogEventLevel.Information => 0x00000000,
+ LogEventLevel.Warning => 0x8A0070EE,
+ LogEventLevel.Fatal => 0xFF00000A,
+ _ => 0x30FFFFFF,
+ };
+
+ private void FrameworkOnUpdate(IFramework framework)
+ {
+ if (this.pendingClearLog)
+ {
+ this.pendingClearLog = false;
+ this.logText.Clear();
+ this.filteredLogEntries.Clear();
+ this.newLogEntries.Clear();
+ }
+
+ if (this.pendingRefilter)
+ {
+ this.pendingRefilter = false;
+ this.filteredLogEntries.Clear();
+ foreach (var log in this.logText)
+ {
+ if (this.IsFilterApplicable(log))
+ this.filteredLogEntries.Add(log);
+ }
+ }
+
+ var numPrevFilteredLogEntries = this.filteredLogEntries.Count;
+ var addedLines = 0;
+ while (this.newLogEntries.TryDequeue(out var logLine))
+ addedLines += this.HandleLogLine(logLine.Line, logLine.LogEvent);
+ this.newRolledLines = addedLines - (this.filteredLogEntries.Count - numPrevFilteredLogEntries);
+ }
+
private void HandleCopyMode(int i, LogEntry line)
{
var selectionChanged = false;
-
+
// If copyStart is -1, it means a drag has not been started yet, let's start one, and select the starting spot.
if (this.copyMode && this.copyStart == -1 && ImGui.IsItemClicked())
{
@@ -334,19 +392,20 @@ internal class ConsoleWindow : Window, IDisposable
}
// Update the selected range when dragging over entries
- if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
+ if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() &&
+ ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
if (!line.SelectedForCopy)
{
- foreach (var index in Enumerable.Range(0, this.FilteredLogEntries.Count))
+ foreach (var index in Enumerable.Range(0, this.filteredLogEntries.Count))
{
if (this.copyStart < i)
{
- this.FilteredLogEntries[index].SelectedForCopy = index >= this.copyStart && index <= i;
+ this.filteredLogEntries[index].SelectedForCopy = index >= this.copyStart && index <= i;
}
else
{
- this.FilteredLogEntries[index].SelectedForCopy = index >= i && index <= this.copyStart;
+ this.filteredLogEntries[index].SelectedForCopy = index >= i && index <= this.copyStart;
}
}
@@ -355,19 +414,37 @@ internal class ConsoleWindow : Window, IDisposable
}
// Finish the drag, we should have already marked all dragged entries as selected by now.
- if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && ImGui.IsMouseReleased(ImGuiMouseButton.Left))
+ if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() &&
+ ImGui.IsMouseReleased(ImGuiMouseButton.Left))
{
this.copyStart = -1;
}
if (selectionChanged)
- {
- var allSelectedLines = this.FilteredLogEntries
- .Where(entry => entry.SelectedForCopy)
- .Select(entry => $"{entry.TimeStamp:HH:mm:ss.fff} {this.GetTextForLogEventLevel(entry.Level)} | {entry.Line}");
+ this.CopyFilteredLogEntries(true);
+ }
- ImGui.SetClipboardText(string.Join("\n", allSelectedLines));
+ private void CopyFilteredLogEntries(bool selectedOnly)
+ {
+ var sb = new StringBuilder();
+ var n = 0;
+ foreach (var entry in this.filteredLogEntries)
+ {
+ if (selectedOnly && !entry.SelectedForCopy)
+ continue;
+
+ n++;
+ sb.AppendLine(entry.ToString());
}
+
+ if (n == 0)
+ return;
+
+ ImGui.SetClipboardText(sb.ToString());
+ Service.Get().AddNotification(
+ $"{n:n0} line(s) copied.",
+ this.WindowName,
+ NotificationType.Success);
}
private void DrawOptionsToolbar()
@@ -384,7 +461,7 @@ internal class ConsoleWindow : Window, IDisposable
EntryPoint.LogLevelSwitch.MinimumLevel = value;
configuration.LogLevel = value;
configuration.QueueSave();
- this.Refilter();
+ this.QueueRefilter();
}
}
@@ -407,18 +484,27 @@ internal class ConsoleWindow : Window, IDisposable
this.settingsPopupWasOpen = settingsPopup;
- if (this.DrawToggleButtonWithTooltip("show_settings", "Show settings", FontAwesomeIcon.List, ref settingsPopup)) ImGui.OpenPopup("##console_settings");
+ if (this.DrawToggleButtonWithTooltip("show_settings", "Show settings", FontAwesomeIcon.List, ref settingsPopup))
+ ImGui.OpenPopup("##console_settings");
ImGui.SameLine();
- if (this.DrawToggleButtonWithTooltip("show_filters", "Show filter toolbar", FontAwesomeIcon.Search, ref this.showFilterToolbar))
+ if (this.DrawToggleButtonWithTooltip(
+ "show_filters",
+ "Show filter toolbar",
+ FontAwesomeIcon.Search,
+ ref this.showFilterToolbar))
{
this.showFilterToolbar = !this.showFilterToolbar;
}
ImGui.SameLine();
- if (this.DrawToggleButtonWithTooltip("show_uncaught_exceptions", "Show uncaught exception while filtering", FontAwesomeIcon.Bug, ref this.filterShowUncaughtExceptions))
+ if (this.DrawToggleButtonWithTooltip(
+ "show_uncaught_exceptions",
+ "Show uncaught exception while filtering",
+ FontAwesomeIcon.Bug,
+ ref this.filterShowUncaughtExceptions))
{
this.filterShowUncaughtExceptions = !this.filterShowUncaughtExceptions;
}
@@ -427,28 +513,33 @@ internal class ConsoleWindow : Window, IDisposable
if (ImGuiComponents.IconButton("clear_log", FontAwesomeIcon.Trash))
{
- this.clearLog = true;
+ this.QueueClear();
}
if (ImGui.IsItemHovered()) ImGui.SetTooltip("Clear Log");
ImGui.SameLine();
- if (this.DrawToggleButtonWithTooltip("copy_mode", "Enable Copy Mode\nRight-click to copy entire log", FontAwesomeIcon.Copy, ref this.copyMode))
+ if (this.DrawToggleButtonWithTooltip(
+ "copy_mode",
+ "Enable Copy Mode\nRight-click to copy entire log",
+ FontAwesomeIcon.Copy,
+ ref this.copyMode))
{
this.copyMode = !this.copyMode;
if (!this.copyMode)
{
- foreach (var entry in this.FilteredLogEntries)
+ foreach (var entry in this.filteredLogEntries)
{
entry.SelectedForCopy = false;
}
}
}
- if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) this.copyLog = true;
-
+ if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
+ this.CopyFilteredLogEntries(false);
+
ImGui.SameLine();
if (this.killGameArmed)
{
@@ -464,16 +555,59 @@ internal class ConsoleWindow : Window, IDisposable
if (ImGui.IsItemHovered()) ImGui.SetTooltip("Kill game");
ImGui.SameLine();
- ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - (200.0f * ImGuiHelpers.GlobalScale));
+ ImGui.SetCursorPosX(
+ ImGui.GetContentRegionMax().X - (2 * 200.0f * ImGuiHelpers.GlobalScale) - ImGui.GetStyle().ItemSpacing.X);
+
ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale);
- if (ImGui.InputTextWithHint("##global_filter", "regex global filter", ref this.textFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll))
+ if (ImGui.InputTextWithHint(
+ "##textHighlight",
+ "regex highlight",
+ ref this.textHighlight,
+ 2048,
+ ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
+ || ImGui.IsItemDeactivatedAfterEdit())
{
- this.Refilter();
+ this.compiledLogHighlight = null;
+ this.exceptionLogHighlight = null;
+ try
+ {
+ if (this.textHighlight != string.Empty)
+ this.compiledLogHighlight = new(this.textHighlight, RegexOptions.IgnoreCase);
+ }
+ catch (Exception e)
+ {
+ this.exceptionLogHighlight = e;
+ }
+
+ foreach (var log in this.logText)
+ log.HighlightMatches = null;
}
- if (ImGui.IsItemDeactivatedAfterEdit())
+ ImGui.SameLine();
+ ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale);
+ if (ImGui.InputTextWithHint(
+ "##textFilter",
+ "regex global filter",
+ ref this.textFilter,
+ 2048,
+ ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
+ || ImGui.IsItemDeactivatedAfterEdit())
{
- this.Refilter();
+ this.compiledLogFilter = null;
+ this.exceptionLogFilter = null;
+ try
+ {
+ this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase);
+
+ this.QueueRefilter();
+ }
+ catch (Exception e)
+ {
+ this.exceptionLogFilter = e;
+ }
+
+ foreach (var log in this.logText)
+ log.HighlightMatches = null;
}
}
@@ -509,9 +643,12 @@ internal class ConsoleWindow : Window, IDisposable
if (!this.showFilterToolbar) return;
PluginFilterEntry? removalEntry = null;
- using var table = ImRaii.Table("plugin_filter_entries", 4, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV);
+ using var table = ImRaii.Table(
+ "plugin_filter_entries",
+ 4,
+ ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV);
if (!table) return;
-
+
ImGui.TableSetupColumn("##remove_button", ImGuiTableColumnFlags.WidthFixed, 25.0f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("##source_name", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("##log_level", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale);
@@ -522,15 +659,16 @@ internal class ConsoleWindow : Window, IDisposable
{
if (this.pluginFilters.All(entry => entry.Source != this.selectedSource))
{
- this.pluginFilters.Add(new PluginFilterEntry
- {
- Source = this.selectedSource,
- Filter = string.Empty,
- Level = LogEventLevel.Debug,
- });
+ this.pluginFilters.Add(
+ new PluginFilterEntry
+ {
+ Source = this.selectedSource,
+ Filter = string.Empty,
+ Level = LogEventLevel.Debug,
+ });
}
- this.Refilter();
+ this.QueueRefilter();
}
ImGui.TableNextColumn();
@@ -541,13 +679,17 @@ internal class ConsoleWindow : Window, IDisposable
.Select(p => p.Manifest.InternalName)
.OrderBy(s => s)
.Prepend("DalamudInternal")
- .Where(name => this.pluginFilter is "" || new FuzzyMatcher(this.pluginFilter.ToLowerInvariant(), MatchMode.Fuzzy).Matches(name.ToLowerInvariant()) != 0)
+ .Where(
+ name => this.pluginFilter is "" || new FuzzyMatcher(
+ this.pluginFilter.ToLowerInvariant(),
+ MatchMode.Fuzzy).Matches(name.ToLowerInvariant()) !=
+ 0)
.ToList();
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
ImGui.InputTextWithHint("##PluginSearchFilter", "Filter Plugin List", ref this.pluginFilter, 2048);
ImGui.Separator();
-
+
if (!sourceNames.Any())
{
ImGui.TextColored(ImGuiColors.DalamudRed, "No Results");
@@ -569,25 +711,27 @@ internal class ConsoleWindow : Window, IDisposable
foreach (var entry in this.pluginFilters)
{
+ ImGui.PushID(entry.Source);
+
ImGui.TableNextColumn();
- if (ImGuiComponents.IconButton($"remove{entry.Source}", FontAwesomeIcon.Trash))
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash))
{
removalEntry = entry;
}
ImGui.TableNextColumn();
ImGui.Text(entry.Source);
-
+
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
- if (ImGui.BeginCombo($"##levels{entry.Source}", $"{entry.Level}+"))
+ if (ImGui.BeginCombo("##levels", $"{entry.Level}+"))
{
foreach (var value in Enum.GetValues())
{
if (ImGui.Selectable(value.ToString(), value == entry.Level))
{
entry.Level = value;
- this.Refilter();
+ this.QueueRefilter();
}
}
@@ -597,19 +741,26 @@ internal class ConsoleWindow : Window, IDisposable
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
var entryFilter = entry.Filter;
- if (ImGui.InputTextWithHint($"##filter{entry.Source}", $"{entry.Source} regex filter", ref entryFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll))
+ if (ImGui.InputTextWithHint(
+ "##filter",
+ $"{entry.Source} regex filter",
+ ref entryFilter,
+ 2048,
+ ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
+ || ImGui.IsItemDeactivatedAfterEdit())
{
entry.Filter = entryFilter;
- this.Refilter();
+ if (entry.FilterException is null)
+ this.QueueRefilter();
}
- if (ImGui.IsItemDeactivatedAfterEdit()) this.Refilter();
+ ImGui.PopID();
}
if (removalEntry is { } toRemove)
{
this.pluginFilters.Remove(toRemove);
- this.Refilter();
+ this.QueueRefilter();
}
}
@@ -636,7 +787,7 @@ internal class ConsoleWindow : Window, IDisposable
if (this.commandText is "clear" or "cls")
{
- this.Clear();
+ this.QueueClear();
return;
}
@@ -717,16 +868,22 @@ internal class ConsoleWindow : Window, IDisposable
return 0;
}
- private void AddAndFilter(string line, LogEvent logEvent, bool isMultiline)
+ /// Add a log entry to the display.
+ /// The line to add.
+ /// The Serilog event associated with this line.
+ /// Number of lines added to .
+ private int HandleLogLine(string line, LogEvent logEvent)
{
- if (line.StartsWith("TROUBLESHOOTING:") || line.StartsWith("LASTEXCEPTION:"))
- return;
+ ThreadSafety.DebugAssertMainThread();
+ // These lines are too huge, and only useful for troubleshooting after the game exist.
+ if (line.StartsWith("TROUBLESHOOTING:") || line.StartsWith("LASTEXCEPTION:"))
+ return 0;
+
+ // Create a log entry template.
var entry = new LogEntry
{
- IsMultiline = isMultiline,
Level = logEvent.Level,
- Line = line,
TimeStamp = logEvent.Timestamp,
HasException = logEvent.Exception != null,
};
@@ -741,98 +898,118 @@ internal class ConsoleWindow : Window, IDisposable
entry.Source = sourceValue;
}
+ var ssp = line.AsSpan();
+ var numLines = 0;
+ while (true)
+ {
+ var next = ssp.IndexOfAny('\r', '\n');
+ if (next == -1)
+ {
+ // Last occurrence; transfer the ownership of the new entry to the queue.
+ entry.Line = ssp.ToString();
+ numLines += this.AddAndFilter(entry);
+ break;
+ }
+
+ // There will be more; create a clone of the entry with the current line.
+ numLines += this.AddAndFilter(entry with { Line = ssp[..next].ToString() });
+
+ // Mark further lines as multiline.
+ entry.IsMultiline = true;
+
+ // Skip the detected line break.
+ ssp = ssp[next..];
+ ssp = ssp.StartsWith("\r\n") ? ssp[2..] : ssp[1..];
+ }
+
+ return numLines;
+ }
+
+ /// Adds a line to the log list and the filtered log list accordingly.
+ /// The new log entry to add.
+ /// Number of lines added to .
+ private int AddAndFilter(LogEntry entry)
+ {
+ ThreadSafety.DebugAssertMainThread();
+
this.logText.Add(entry);
- var avoidScroll = this.FilteredLogEntries.Count == this.FilteredLogEntries.Size;
- if (this.IsFilterApplicable(entry))
- {
- this.FilteredLogEntries.Add(entry);
- if (avoidScroll) Interlocked.Increment(ref this.newRolledLines);
- }
+ if (!this.IsFilterApplicable(entry))
+ return 0;
+
+ this.filteredLogEntries.Add(entry);
+ return 1;
}
+ /// Determines if a log entry passes the user-specified filter.
+ /// The entry to test.
+ /// true if it passes the filter.
private bool IsFilterApplicable(LogEntry entry)
{
- if (this.regexError)
+ ThreadSafety.DebugAssertMainThread();
+
+ if (this.exceptionLogFilter is not null)
return false;
- try
+ // If this entry is below a newly set minimum level, fail it
+ if (EntryPoint.LogLevelSwitch.MinimumLevel > entry.Level)
+ return false;
+
+ // Show exceptions that weren't properly tagged with a Source (generally meaning they were uncaught)
+ // After log levels because uncaught exceptions should *never* fall below Error.
+ if (this.filterShowUncaughtExceptions && entry.HasException && entry.Source == null)
+ return true;
+
+ // (global filter) && (plugin filter) must be satisfied.
+ var wholeCond = true;
+
+ // If we have a global filter, check that first
+ if (this.compiledLogFilter is { } logFilter)
{
- // If this entry is below a newly set minimum level, fail it
- if (EntryPoint.LogLevelSwitch.MinimumLevel > entry.Level)
- return false;
-
- // Show exceptions that weren't properly tagged with a Source (generally meaning they were uncaught)
- // After log levels because uncaught exceptions should *never* fall below Error.
- if (this.filterShowUncaughtExceptions && entry.HasException && entry.Source == null)
- return true;
+ // Someone will definitely try to just text filter a source without using the actual filters, should allow that.
+ var matchesSource = entry.Source is not null && logFilter.IsMatch(entry.Source);
+ var matchesContent = logFilter.IsMatch(entry.Line);
- // If we have a global filter, check that first
- if (!this.textFilter.IsNullOrEmpty())
+ wholeCond &= matchesSource || matchesContent;
+ }
+
+ // If this entry has a filter, check the filter
+ if (this.pluginFilters.Count > 0)
+ {
+ var matchesAny = false;
+
+ foreach (var filterEntry in this.pluginFilters)
{
- // Someone will definitely try to just text filter a source without using the actual filters, should allow that.
- var matchesSource = entry.Source is not null && Regex.IsMatch(entry.Source, this.textFilter, RegexOptions.IgnoreCase);
- var matchesContent = Regex.IsMatch(entry.Line, this.textFilter, RegexOptions.IgnoreCase);
+ if (!string.Equals(filterEntry.Source, entry.Source, StringComparison.InvariantCultureIgnoreCase))
+ continue;
- return matchesSource || matchesContent;
- }
-
- // If this entry has a filter, check the filter
- if (this.pluginFilters.FirstOrDefault(filter => string.Equals(filter.Source, entry.Source, StringComparison.InvariantCultureIgnoreCase)) is { } filterEntry)
- {
var allowedLevel = filterEntry.Level <= entry.Level;
- var matchesContent = filterEntry.Filter.IsNullOrEmpty() || Regex.IsMatch(entry.Line, filterEntry.Filter, RegexOptions.IgnoreCase);
+ var matchesContent = filterEntry.FilterRegex?.IsMatch(entry.Line) is not false;
- return allowedLevel && matchesContent;
+ matchesAny |= allowedLevel && matchesContent;
+ if (matchesAny)
+ break;
}
- }
- catch (Exception)
- {
- this.regexError = true;
- return false;
+
+ wholeCond &= matchesAny;
}
- // else we couldn't find a filter for this entry, if we have any filters, we need to block this entry.
- return !this.pluginFilters.Any();
+ return wholeCond;
}
- private void Refilter()
- {
- lock (this.renderLock)
- {
- this.regexError = false;
- this.FilteredLogEntries = new RollingList(this.logText.Where(this.IsFilterApplicable), Math.Max(LogLinesMinimum, this.logLinesLimit));
- }
- }
+ /// Queues clearing the window of all log entries, before next call to .
+ private void QueueClear() => this.pendingClearLog = true;
- private string GetTextForLogEventLevel(LogEventLevel level) => level switch
- {
- LogEventLevel.Error => "ERR",
- LogEventLevel.Verbose => "VRB",
- LogEventLevel.Debug => "DBG",
- LogEventLevel.Information => "INF",
- LogEventLevel.Warning => "WRN",
- LogEventLevel.Fatal => "FTL",
- _ => throw new ArgumentOutOfRangeException(level.ToString(), "Invalid LogEventLevel"),
- };
+ /// Queues filtering the log entries again, before next call to .
+ private void QueueRefilter() => this.pendingRefilter = true;
- private uint GetColorForLogEventLevel(LogEventLevel level) => level switch
- {
- LogEventLevel.Error => 0x800000EE,
- LogEventLevel.Verbose => 0x00000000,
- LogEventLevel.Debug => 0x00000000,
- LogEventLevel.Information => 0x00000000,
- LogEventLevel.Warning => 0x8A0070EE,
- LogEventLevel.Fatal => 0xFF00000A,
- _ => throw new ArgumentOutOfRangeException(level.ToString(), "Invalid LogEventLevel"),
- };
+ /// 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 void OnLogLine(object sender, (string Line, LogEvent LogEvent) logEvent)
- {
- this.HandleLogLine(logEvent.Line, logEvent.LogEvent);
- }
-
- private bool DrawToggleButtonWithTooltip(string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState)
+ private bool DrawToggleButtonWithTooltip(
+ string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState)
{
var result = false;
@@ -855,36 +1032,120 @@ internal class ConsoleWindow : Window, IDisposable
this.logLinesLimit = dalamudConfiguration.LogLinesLimit;
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
this.logText.Size = limit;
- this.FilteredLogEntries.Size = limit;
+ this.filteredLogEntries.Size = limit;
}
- private class LogEntry
+ private unsafe void DrawHighlighted(
+ ReadOnlySpan line,
+ MatchCollection matches,
+ uint col,
+ uint highlightCol)
{
- public string Line { get; init; } = string.Empty;
+ Span charOffsets = stackalloc int[(matches.Count * 2) + 2];
+ var charOffsetsIndex = 1;
+ for (var j = 0; j < matches.Count; j++)
+ {
+ var g = matches[j].Groups[0];
+ charOffsets[charOffsetsIndex++] = g.Index;
+ charOffsets[charOffsetsIndex++] = g.Index + g.Length;
+ }
+
+ charOffsets[charOffsetsIndex++] = line.Length;
+
+ var screenPos = ImGui.GetCursorScreenPos();
+ var drawList = ImGui.GetWindowDrawList().NativePtr;
+ var font = ImGui.GetFont();
+ var size = ImGui.GetFontSize();
+ var scale = size / font.FontSize;
+ var hotData = font.IndexedHotDataWrapped();
+ var lookup = font.IndexLookupWrapped();
+ var kern = (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NoKerning) == 0;
+ var lastc = '\0';
+ for (var i = 0; i < charOffsetsIndex - 1; i++)
+ {
+ var begin = charOffsets[i];
+ var end = charOffsets[i + 1];
+ if (begin == end)
+ continue;
+
+ for (var j = begin; j < end; j++)
+ {
+ var currc = line[j];
+ if (currc >= lookup.Length || lookup[currc] == ushort.MaxValue)
+ currc = (char)font.FallbackChar;
+
+ if (kern)
+ screenPos.X += scale * ImGui.GetFont().GetDistanceAdjustmentForPair(lastc, currc);
+ font.RenderChar(drawList, size, screenPos, i % 2 == 1 ? highlightCol : col, currc);
+
+ screenPos.X += scale * hotData[currc].AdvanceX;
+ lastc = currc;
+ }
+ }
+ }
+
+ private record LogEntry
+ {
+ public string Line { get; set; } = string.Empty;
public LogEventLevel Level { get; init; }
public DateTimeOffset TimeStamp { get; init; }
- public bool IsMultiline { get; init; }
+ public bool IsMultiline { get; set; }
///
/// Gets or sets the system responsible for generating this log entry. Generally will be a plugin's
/// InternalName.
///
public string? Source { get; set; }
-
+
public bool SelectedForCopy { get; set; }
public bool HasException { get; init; }
+
+ public MatchCollection? HighlightMatches { get; set; }
+
+ public string TimestampString => this.TimeStamp.ToString("HH:mm:ss.fff");
+
+ public override string ToString() =>
+ this.IsMultiline
+ ? $"\t{this.Line}"
+ : $"{this.TimestampString} | {GetTextForLogEventLevel(this.Level)} | {this.Line}";
}
private class PluginFilterEntry
{
+ private string filter = string.Empty;
+
public string Source { get; init; } = string.Empty;
- public string Filter { get; set; } = string.Empty;
-
+ public string Filter
+ {
+ get => this.filter;
+ set
+ {
+ this.filter = value;
+ this.FilterRegex = null;
+ this.FilterException = null;
+ if (value == string.Empty)
+ return;
+
+ try
+ {
+ this.FilterRegex = new(value, RegexOptions.IgnoreCase);
+ }
+ catch (Exception e)
+ {
+ this.FilterException = e;
+ }
+ }
+ }
+
public LogEventLevel Level { get; set; }
+
+ public Regex? FilterRegex { get; private set; }
+
+ public Exception? FilterException { get; private set; }
}
}
diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs
deleted file mode 100644
index ecaa522e5..000000000
--- a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs
+++ /dev/null
@@ -1,266 +0,0 @@
-using System.Numerics;
-
-using Dalamud.Interface.Windowing;
-
-using ImGuiNET;
-
-namespace Dalamud.Interface.Internal.Windows;
-
-///
-/// A window for displaying IME details.
-///
-internal unsafe class DalamudImeWindow : Window
-{
- private const int ImePageSize = 9;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public DalamudImeWindow()
- : base(
- "Dalamud IME",
- ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoBackground)
- {
- this.Size = default(Vector2);
-
- this.RespectCloseHotkey = false;
- }
-
- ///
- public override void Draw()
- {
- }
-
- ///
- public override void PostDraw()
- {
- if (Service.GetNullable() is not { } ime)
- return;
-
- var viewport = ime.AssociatedViewport;
- if (viewport.NativePtr is null)
- return;
-
- var drawCand = ime.ImmCand.Count != 0;
- var drawConv = drawCand || ime.ShowPartialConversion;
- var drawIme = ime.InputModeIcon != 0;
- var imeIconFont = InterfaceManager.DefaultFont;
-
- var pad = ImGui.GetStyle().WindowPadding;
- var candTextSize = ImGui.CalcTextSize(ime.ImmComp == string.Empty ? " " : ime.ImmComp);
-
- var native = ime.ImmCandNative;
- var totalIndex = native.dwSelection + 1;
- var totalSize = native.dwCount;
-
- var pageStart = native.dwPageStart;
- var pageIndex = (pageStart / ImePageSize) + 1;
- var pageCount = (totalSize / ImePageSize) + 1;
- var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})";
-
- // Calc the window size.
- var maxTextWidth = 0f;
- for (var i = 0; i < ime.ImmCand.Count; i++)
- {
- var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.ImmCand[i]}");
- maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X;
- }
-
- maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X;
- maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.ImmComp).X
- ? maxTextWidth
- : ImGui.CalcTextSize(ime.ImmComp).X;
-
- var numEntries = (drawCand ? ime.ImmCand.Count + 1 : 0) + 1 + (drawIme ? 1 : 0);
- var spaceY = ImGui.GetStyle().ItemSpacing.Y;
- var imeWindowHeight = (spaceY * (numEntries - 1)) + (candTextSize.Y * numEntries);
- var windowSize = new Vector2(maxTextWidth, imeWindowHeight) + (pad * 2);
-
- // 1. Figure out the expanding direction.
- var expandUpward = ime.CursorPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y;
- var windowPos = ime.CursorPos - pad;
- if (expandUpward)
- {
- windowPos.Y -= windowSize.Y - candTextSize.Y - (pad.Y * 2);
- if (drawIme)
- windowPos.Y += candTextSize.Y + spaceY;
- }
- else
- {
- if (drawIme)
- windowPos.Y -= candTextSize.Y + spaceY;
- }
-
- // 2. Contain within the viewport. Do not use clamp, as the target window might be too small.
- if (windowPos.X < viewport.WorkPos.X)
- windowPos.X = viewport.WorkPos.X;
- else if (windowPos.X + windowSize.X > viewport.WorkPos.X + viewport.WorkSize.X)
- windowPos.X = (viewport.WorkPos.X + viewport.WorkSize.X) - windowSize.X;
- if (windowPos.Y < viewport.WorkPos.Y)
- windowPos.Y = viewport.WorkPos.Y;
- else if (windowPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y)
- windowPos.Y = (viewport.WorkPos.Y + viewport.WorkSize.Y) - windowSize.Y;
-
- var cursor = windowPos + pad;
-
- // Draw the ime window.
- var drawList = ImGui.GetForegroundDrawList(viewport);
-
- // Draw the background rect for candidates.
- if (drawCand)
- {
- Vector2 candRectLt, candRectRb;
- if (!expandUpward)
- {
- candRectLt = windowPos + candTextSize with { X = 0 } + pad with { X = 0 };
- candRectRb = windowPos + windowSize;
- if (drawIme)
- candRectLt.Y += spaceY + candTextSize.Y;
- }
- else
- {
- candRectLt = windowPos;
- candRectRb = windowPos + (windowSize - candTextSize with { X = 0 } - pad with { X = 0 });
- if (drawIme)
- candRectRb.Y -= spaceY + candTextSize.Y;
- }
-
- drawList.AddRectFilled(
- candRectLt,
- candRectRb,
- ImGui.GetColorU32(ImGuiCol.WindowBg),
- ImGui.GetStyle().WindowRounding);
- }
-
- if (!expandUpward && drawIme)
- {
- for (var dx = -2; dx <= 2; dx++)
- {
- for (var dy = -2; dy <= 2; dy++)
- {
- if (dx != 0 || dy != 0)
- {
- imeIconFont.RenderChar(
- drawList,
- imeIconFont.FontSize,
- cursor + new Vector2(dx, dy),
- ImGui.GetColorU32(ImGuiCol.WindowBg),
- ime.InputModeIcon);
- }
- }
- }
-
- imeIconFont.RenderChar(
- drawList,
- imeIconFont.FontSize,
- cursor,
- ImGui.GetColorU32(ImGuiCol.Text),
- ime.InputModeIcon);
- cursor.Y += candTextSize.Y + spaceY;
- }
-
- if (!expandUpward && drawConv)
- {
- DrawTextBeingConverted();
- cursor.Y += candTextSize.Y + spaceY;
-
- // Add a separator.
- drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator));
- }
-
- if (drawCand)
- {
- // Add the candidate words.
- for (var i = 0; i < ime.ImmCand.Count; i++)
- {
- var selected = i == (native.dwSelection % ImePageSize);
- var color = ImGui.GetColorU32(ImGuiCol.Text);
- if (selected)
- color = ImGui.GetColorU32(ImGuiCol.NavHighlight);
-
- drawList.AddText(cursor, color, $"{i + 1}. {ime.ImmCand[i]}");
- cursor.Y += candTextSize.Y + spaceY;
- }
-
- // Add a separator
- drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator));
-
- // Add the pages infomation.
- drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), pageInfo);
- cursor.Y += candTextSize.Y + spaceY;
- }
-
- if (expandUpward && drawConv)
- {
- // Add a separator.
- drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator));
-
- DrawTextBeingConverted();
- cursor.Y += candTextSize.Y + spaceY;
- }
-
- if (expandUpward && drawIme)
- {
- for (var dx = -2; dx <= 2; dx++)
- {
- for (var dy = -2; dy <= 2; dy++)
- {
- if (dx != 0 || dy != 0)
- {
- imeIconFont.RenderChar(
- drawList,
- imeIconFont.FontSize,
- cursor + new Vector2(dx, dy),
- ImGui.GetColorU32(ImGuiCol.WindowBg),
- ime.InputModeIcon);
- }
- }
- }
-
- imeIconFont.RenderChar(
- drawList,
- imeIconFont.FontSize,
- cursor,
- ImGui.GetColorU32(ImGuiCol.Text),
- ime.InputModeIcon);
- }
-
- return;
-
- void DrawTextBeingConverted()
- {
- // Draw the text background.
- drawList.AddRectFilled(
- cursor - (pad / 2),
- cursor + candTextSize + (pad / 2),
- ImGui.GetColorU32(ImGuiCol.WindowBg));
-
- // If only a part of the full text is marked for conversion, then draw background for the part being edited.
- if (ime.PartialConversionFrom != 0 || ime.PartialConversionTo != ime.ImmComp.Length)
- {
- var part1 = ime.ImmComp[..ime.PartialConversionFrom];
- var part2 = ime.ImmComp[..ime.PartialConversionTo];
- var size1 = ImGui.CalcTextSize(part1);
- var size2 = ImGui.CalcTextSize(part2);
- drawList.AddRectFilled(
- cursor + size1 with { Y = 0 },
- cursor + size2,
- ImGui.GetColorU32(ImGuiCol.TextSelectedBg));
- }
-
- // Add the text being converted.
- drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.ImmComp);
-
- // Draw the caret inside the composition string.
- if (DalamudIme.ShowCursorInInputText)
- {
- var partBeforeCaret = ime.ImmComp[..ime.CompositionCursorOffset];
- var sizeBeforeCaret = ImGui.CalcTextSize(partBeforeCaret);
- drawList.AddLine(
- cursor + sizeBeforeCaret with { Y = 0 },
- cursor + sizeBeforeCaret,
- ImGui.GetColorU32(ImGuiCol.Text));
- }
- }
- }
-}
diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs
index 3ff7cde76..acd7c2b6f 100644
--- a/Dalamud/ServiceManager.cs
+++ b/Dalamud/ServiceManager.cs
@@ -165,6 +165,7 @@ internal static class ServiceManager
var earlyLoadingServices = new HashSet();
var blockingEarlyLoadingServices = new HashSet();
+ var providedServices = new HashSet();
var dependencyServicesMap = new Dictionary>();
var getAsyncTaskMap = new Dictionary();
@@ -197,7 +198,10 @@ internal static class ServiceManager
// We don't actually need to load provided services, something else does
if (serviceKind.HasFlag(ServiceKind.ProvidedService))
+ {
+ providedServices.Add(serviceType);
continue;
+ }
Debug.Assert(
serviceKind.HasFlag(ServiceKind.EarlyLoadedService) ||
@@ -340,7 +344,16 @@ internal static class ServiceManager
}
if (!tasks.Any())
- throw new InvalidOperationException("Unresolvable dependency cycle detected");
+ {
+ // No more services we can start loading for now.
+ // Either we're waiting for provided services, or there's a dependency cycle.
+ providedServices.RemoveWhere(x => getAsyncTaskMap[x].IsCompleted);
+ if (providedServices.Any())
+ await Task.WhenAny(providedServices.Select(x => getAsyncTaskMap[x]));
+ else
+ throw new InvalidOperationException("Unresolvable dependency cycle detected");
+ continue;
+ }
if (servicesToLoad.Any())
{
diff --git a/Dalamud/Utility/ThreadSafety.cs b/Dalamud/Utility/ThreadSafety.cs
index 7c4b0dfcb..ce3ddc602 100644
--- a/Dalamud/Utility/ThreadSafety.cs
+++ b/Dalamud/Utility/ThreadSafety.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.CompilerServices;
namespace Dalamud.Utility;
@@ -19,6 +20,7 @@ public static class ThreadSafety
/// Throws an exception when the current thread is not the main thread.
///
/// Thrown when the current thread is not the main thread.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AssertMainThread()
{
if (!threadStaticIsMainThread)
@@ -31,6 +33,7 @@ public static class ThreadSafety
/// Throws an exception when the current thread is the main thread.
///
/// Thrown when the current thread is the main thread.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AssertNotMainThread()
{
if (threadStaticIsMainThread)
@@ -39,6 +42,15 @@ public static class ThreadSafety
}
}
+ /// , but only on debug compilation mode.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void DebugAssertMainThread()
+ {
+#if DEBUG
+ AssertMainThread();
+#endif
+ }
+
///
/// Marks a thread as the main thread.
///
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index 722a2c512..ac2ced26f 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit 722a2c512238ac4b5324e3d343b316d8c8633a02
+Subproject commit ac2ced26fc98153c65f5b8f0eaf0f464258ff683