diff --git a/Dalamud/Game/Addon/Events/AddonEventType.cs b/Dalamud/Game/Addon/Events/AddonEventType.cs
index 25beb13fc..9e062a9d0 100644
--- a/Dalamud/Game/Addon/Events/AddonEventType.cs
+++ b/Dalamud/Game/Addon/Events/AddonEventType.cs
@@ -61,6 +61,11 @@ public enum AddonEventType : byte
///
InputBaseInputReceived = 15,
+ ///
+ /// Fired at the very beginning of AtkInputManager.HandleInput on AtkStage.ViewportEventManager. Used in LovmMiniMap.
+ ///
+ RawInputData = 16,
+
///
/// Focus Start.
///
@@ -107,7 +112,12 @@ public enum AddonEventType : byte
SliderReleased = 30,
///
- /// AtkComponentList RollOver.
+ /// AtkComponentList Button Press.
+ ///
+ ListButtonPress = 31,
+
+ ///
+ /// AtkComponentList Roll Over.
///
ListItemRollOver = 33,
@@ -126,11 +136,31 @@ public enum AddonEventType : byte
///
ListItemDoubleClick = 36,
+ ///
+ /// AtkComponentList Highlight.
+ ///
+ ListItemHighlight = 37,
+
///
/// AtkComponentList Select.
///
ListItemSelect = 38,
+ ///
+ /// AtkComponentList Pad Drag Drop Begin.
+ ///
+ ListItemPadDragDropBegin = 40,
+
+ ///
+ /// AtkComponentList Pad Drag Drop End.
+ ///
+ ListItemPadDragDropEnd = 41,
+
+ ///
+ /// AtkComponentList Pad Drag Drop Insert.
+ ///
+ ListItemPadDragDropInsert = 42,
+
///
/// AtkComponentDragDrop Begin.
/// Sent on MouseDown over a draggable icon (will NOT send for a locked icon).
@@ -142,12 +172,22 @@ public enum AddonEventType : byte
///
DragDropEnd = 51,
+ ///
+ /// AtkComponentDragDrop Insert Attempt.
+ ///
+ DragDropInsertAttempt = 52,
+
///
/// AtkComponentDragDrop Insert.
/// Sent when dropping an icon into a hotbar/inventory slot or similar.
///
DragDropInsert = 53,
+ ///
+ /// AtkComponentDragDrop Can Accept Check.
+ ///
+ DragDropCanAcceptCheck = 54,
+
///
/// AtkComponentDragDrop Roll Over.
///
@@ -165,23 +205,18 @@ public enum AddonEventType : byte
DragDropDiscard = 57,
///
- /// Drag Drop Unknown.
+ /// AtkComponentDragDrop Click.
+ /// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
///
- [Obsolete("Use DragDropDiscard", true)]
- DragDropUnk54 = 54,
+ DragDropClick = 58,
///
/// AtkComponentDragDrop Cancel.
/// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
///
+ [Obsolete("Renamed to DragDropClick")]
DragDropCancel = 58,
- ///
- /// Drag Drop Unknown.
- ///
- [Obsolete("Use DragDropCancel", true)]
- DragDropUnk55 = 55,
-
///
/// AtkComponentIconText Roll Over.
///
@@ -217,6 +252,11 @@ public enum AddonEventType : byte
///
TimerEnd = 65,
+ ///
+ /// AtkTimer Start.
+ ///
+ TimerStart = 66,
+
///
/// AtkSimpleTween Progress.
///
@@ -247,6 +287,11 @@ public enum AddonEventType : byte
///
WindowChangeScale = 72,
+ ///
+ /// AtkTimeline Active Label Changed.
+ ///
+ TimelineActiveLabelChanged = 75,
+
///
/// AtkTextNode Link Mouse Click.
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index 64c005b5f..c70c0c10f 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -27,7 +27,11 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
private static readonly ModuleLog Log = ModuleLog.Create();
+ [ServiceManager.ServiceDependency]
+ private readonly Framework framework = Service.Get();
+
private Hook? onInitializeAddonHook;
+ private bool isInvokingListeners = false;
[ServiceManager.ServiceConstructor]
private AddonLifecycle()
@@ -58,20 +62,23 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// The listener to register.
internal void RegisterListener(AddonLifecycleEventListener listener)
{
- if (!this.EventListeners.ContainsKey(listener.EventType))
+ this.framework.RunOnTick(() =>
{
- if (!this.EventListeners.TryAdd(listener.EventType, []))
- return;
- }
+ if (!this.EventListeners.ContainsKey(listener.EventType))
+ {
+ if (!this.EventListeners.TryAdd(listener.EventType, []))
+ return;
+ }
- // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
- if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
- {
- if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
- return;
- }
+ // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
+ if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
+ {
+ if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
+ return;
+ }
- this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
+ this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
+ }, delayTicks: this.isInvokingListeners ? 1 : 0);
}
///
@@ -80,13 +87,16 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// The listener to unregister.
internal void UnregisterListener(AddonLifecycleEventListener listener)
{
- if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
+ this.framework.RunOnTick(() =>
{
- if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
+ if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
{
- addonListener.Remove(listener);
+ if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
+ {
+ addonListener.Remove(listener);
+ }
}
- }
+ }, delayTicks: this.isInvokingListeners ? 1 : 0);
}
///
@@ -97,6 +107,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// What to blame on errors.
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
+ this.isInvokingListeners = true;
+
// Early return if we don't have any listeners of this type
if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
@@ -131,6 +143,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
}
}
}
+
+ this.isInvokingListeners = false;
}
///
diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs
new file mode 100644
index 000000000..c772783a1
--- /dev/null
+++ b/Dalamud/Game/Chat/LogMessage.cs
@@ -0,0 +1,221 @@
+using System.Diagnostics.CodeAnalysis;
+
+using Dalamud.Data;
+using Dalamud.Utility;
+
+using FFXIVClientStructs.FFXIV.Client.System.String;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+using FFXIVClientStructs.FFXIV.Component.Text;
+using FFXIVClientStructs.Interop;
+
+using Lumina.Excel;
+using Lumina.Text.ReadOnly;
+
+namespace Dalamud.Game.Chat;
+
+///
+/// Interface representing a log message.
+///
+public interface ILogMessage : IEquatable
+{
+ ///
+ /// Gets the address of the log message in memory.
+ ///
+ nint Address { get; }
+
+ ///
+ /// Gets the ID of this log message.
+ ///
+ uint LogMessageId { get; }
+
+ ///
+ /// Gets the GameData associated with this log message.
+ ///
+ RowRef GameData { get; }
+
+ ///
+ /// Gets the entity that is the source of this log message, if any.
+ ///
+ ILogMessageEntity? SourceEntity { get; }
+
+ ///
+ /// Gets the entity that is the target of this log message, if any.
+ ///
+ ILogMessageEntity? TargetEntity { get; }
+
+ ///
+ /// Gets the number of parameters.
+ ///
+ int ParameterCount { get; }
+
+ ///
+ /// Retrieves the value of a parameter for the log message if it is an int.
+ ///
+ /// The index of the parameter to retrieve.
+ /// The value of the parameter.
+ /// if the parameter was retrieved successfully.
+ bool TryGetIntParameter(int index, out int value);
+
+ ///
+ /// Retrieves the value of a parameter for the log message if it is a string.
+ ///
+ /// The index of the parameter to retrieve.
+ /// The value of the parameter.
+ /// if the parameter was retrieved successfully.
+ bool TryGetStringParameter(int index, out ReadOnlySeString value);
+
+ ///
+ /// Formats this log message into an approximation of the string that will eventually be shown in the log.
+ ///
+ /// This can cause side effects such as playing sound effects and thus should only be used for debugging.
+ /// The formatted string.
+ ReadOnlySeString FormatLogMessageForDebugging();
+}
+
+///
+/// This struct represents log message in the queue to be added to the chat.
+///
+/// A pointer to the log message.
+internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessage
+{
+ ///
+ public nint Address => (nint)ptr;
+
+ ///
+ public uint LogMessageId => ptr->LogMessageId;
+
+ ///
+ public RowRef GameData => LuminaUtils.CreateRef(ptr->LogMessageId);
+
+ ///
+ ILogMessageEntity? ILogMessage.SourceEntity => ptr->SourceKind == EntityRelationKind.None ? null : this.SourceEntity;
+
+ ///
+ ILogMessageEntity? ILogMessage.TargetEntity => ptr->TargetKind == EntityRelationKind.None ? null : this.TargetEntity;
+
+ ///
+ public int ParameterCount => ptr->Parameters.Count;
+
+ private LogMessageEntity SourceEntity => new(ptr, true);
+
+ private LogMessageEntity TargetEntity => new(ptr, false);
+
+ public static bool operator ==(LogMessage x, LogMessage y) => x.Equals(y);
+
+ public static bool operator !=(LogMessage x, LogMessage y) => !(x == y);
+
+ ///
+ public bool Equals(ILogMessage? other)
+ {
+ return other is LogMessage logMessage && this.Equals(logMessage);
+ }
+
+ ///
+ public override bool Equals([NotNullWhen(true)] object? obj)
+ {
+ return obj is LogMessage logMessage && this.Equals(logMessage);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(this.LogMessageId, this.SourceEntity, this.TargetEntity);
+ }
+
+ ///
+ public bool TryGetIntParameter(int index, out int value)
+ {
+ value = 0;
+ if (!this.TryGetParameter(index, out var parameter)) return false;
+ if (parameter.Type != TextParameterType.Integer) return false;
+ value = parameter.IntValue;
+ return true;
+ }
+
+ ///
+ public bool TryGetStringParameter(int index, out ReadOnlySeString value)
+ {
+ value = default;
+ if (!this.TryGetParameter(index, out var parameter)) return false;
+ if (parameter.Type == TextParameterType.String)
+ {
+ value = new(parameter.StringValue.AsSpan());
+ return true;
+ }
+
+ if (parameter.Type == TextParameterType.ReferencedUtf8String)
+ {
+ value = new(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan());
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ public ReadOnlySeString FormatLogMessageForDebugging()
+ {
+ var logModule = RaptureLogModule.Instance();
+
+ // the formatting logic is taken from RaptureLogModule_Update
+
+ using var utf8 = new Utf8String();
+ SetName(logModule, this.SourceEntity);
+ SetName(logModule, this.TargetEntity);
+
+ using var rssb = new RentedSeStringBuilder();
+ logModule->RaptureTextModule->FormatString(rssb.Builder.Append(this.GameData.Value.Text).GetViewAsSpan(), &ptr->Parameters, &utf8);
+
+ return new ReadOnlySeString(utf8.AsSpan());
+
+ static void SetName(RaptureLogModule* self, LogMessageEntity item)
+ {
+ var name = item.NameSpan.GetPointer(0);
+
+ if (item.IsPlayer)
+ {
+ var str = self->TempParseMessage.GetPointer(item.IsSourceEntity ? 8 : 9);
+ self->FormatPlayerLink(name, str, null, 0, item.Kind != 1 /* LocalPlayer */, item.HomeWorldId, false, null, false);
+
+ if (item.HomeWorldId != 0 && item.HomeWorldId != AgentLobby.Instance()->LobbyData.HomeWorldId)
+ {
+ var crossWorldSymbol = self->RaptureTextModule->UnkStrings0.GetPointer(3);
+ if (!crossWorldSymbol->StringPtr.HasValue)
+ self->RaptureTextModule->ProcessMacroCode(crossWorldSymbol, "\0"u8);
+ str->Append(crossWorldSymbol);
+ if (self->UIModule->GetWorldHelper()->AllWorlds.TryGetValuePointer(item.HomeWorldId, out var world))
+ str->ConcatCStr(world->Name);
+ }
+
+ name = str->StringPtr;
+ }
+
+ if (item.IsSourceEntity)
+ {
+ self->RaptureTextModule->SetGlobalTempEntity1(name, item.Sex, item.ObjStrId);
+ }
+ else
+ {
+ self->RaptureTextModule->SetGlobalTempEntity2(name, item.Sex, item.ObjStrId);
+ }
+ }
+ }
+
+ private bool TryGetParameter(int index, out TextParameter value)
+ {
+ if (index < 0 || index >= ptr->Parameters.Count)
+ {
+ value = default;
+ return false;
+ }
+
+ value = ptr->Parameters[index];
+ return true;
+ }
+
+ private bool Equals(LogMessage other)
+ {
+ return this.LogMessageId == other.LogMessageId && this.SourceEntity == other.SourceEntity && this.TargetEntity == other.TargetEntity;
+ }
+}
diff --git a/Dalamud/Game/Chat/LogMessageEntity.cs b/Dalamud/Game/Chat/LogMessageEntity.cs
new file mode 100644
index 000000000..91e905928
--- /dev/null
+++ b/Dalamud/Game/Chat/LogMessageEntity.cs
@@ -0,0 +1,113 @@
+using System.Diagnostics.CodeAnalysis;
+
+using Dalamud.Data;
+using Dalamud.Plugin.Services;
+
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+
+using Lumina.Excel;
+using Lumina.Excel.Sheets;
+using Lumina.Text.ReadOnly;
+
+namespace Dalamud.Game.Chat;
+
+///
+/// Interface representing an entity related to a log message.
+///
+public interface ILogMessageEntity : IEquatable
+{
+ ///
+ /// Gets the name of this entity.
+ ///
+ ReadOnlySeString Name { get; }
+
+ ///
+ /// Gets the ID of the homeworld of this entity, if it is a player.
+ ///
+ ushort HomeWorldId { get; }
+
+ ///
+ /// Gets the homeworld of this entity, if it is a player.
+ ///
+ RowRef HomeWorld { get; }
+
+ ///
+ /// Gets the ObjStr ID of this entity, if not a player. See .
+ ///
+ uint ObjStrId { get; }
+
+ ///
+ /// Gets a value indicating whether this entity is a player.
+ ///
+ bool IsPlayer { get; }
+}
+
+///
+/// This struct represents an entity related to a log message.
+///
+/// A pointer to the log message item.
+/// If represents the source entity of the log message, otherwise represents the target entity.
+internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool source) : ILogMessageEntity
+{
+ ///
+ public ReadOnlySeString Name => new(this.NameSpan[..this.NameSpan.IndexOf((byte)0)]);
+
+ ///
+ public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld;
+
+ ///
+ public RowRef HomeWorld => LuminaUtils.CreateRef(this.HomeWorldId);
+
+ ///
+ public uint ObjStrId => source ? ptr->SourceObjStrId : ptr->TargetObjStrId;
+
+ ///
+ public bool IsPlayer => source ? ptr->SourceIsPlayer : ptr->TargetIsPlayer;
+
+ ///
+ /// Gets the Span containing the raw name of this entity.
+ ///
+ internal Span NameSpan => source ? ptr->SourceName : ptr->TargetName;
+
+ ///
+ /// Gets the kind of the entity.
+ ///
+ internal byte Kind => source ? (byte)ptr->SourceKind : (byte)ptr->TargetKind;
+
+ ///
+ /// Gets the Sex of this entity.
+ ///
+ internal byte Sex => source ? ptr->SourceSex : ptr->TargetSex;
+
+ ///
+ /// Gets a value indicating whether this entity is the source entity of a log message.
+ ///
+ internal bool IsSourceEntity => source;
+
+ public static bool operator ==(LogMessageEntity x, LogMessageEntity y) => x.Equals(y);
+
+ public static bool operator !=(LogMessageEntity x, LogMessageEntity y) => !(x == y);
+
+ ///
+ public bool Equals(ILogMessageEntity other)
+ {
+ return other is LogMessageEntity entity && this.Equals(entity);
+ }
+
+ ///
+ public override bool Equals([NotNullWhen(true)] object? obj)
+ {
+ return obj is LogMessageEntity entity && this.Equals(entity);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(this.Name, this.HomeWorldId, this.ObjStrId, this.Sex, this.IsPlayer);
+ }
+
+ private bool Equals(LogMessageEntity other)
+ {
+ return this.Name == other.Name && this.HomeWorldId == other.HomeWorldId && this.ObjStrId == other.ObjStrId && this.Kind == other.Kind && this.Sex == other.Sex && this.IsPlayer == other.IsPlayer;
+ }
+}
diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs
index e9a0a1aae..c514752da 100644
--- a/Dalamud/Game/Gui/ChatGui.cs
+++ b/Dalamud/Game/Gui/ChatGui.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal;
@@ -41,10 +42,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
private readonly Queue chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action> dalamudLinkHandlers = [];
+ private readonly List seenLogMessageObjects = [];
private readonly Hook printMessageHook;
private readonly Hook inventoryItemCopyHook;
private readonly Hook handleLinkClickHook;
+ private readonly Hook handleLogModuleUpdate;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service.Get();
@@ -58,10 +61,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
this.printMessageHook = Hook.FromAddress(RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
this.inventoryItemCopyHook = Hook.FromAddress((nint)InventoryItem.StaticVirtualTablePointer->Copy, this.InventoryItemCopyDetour);
this.handleLinkClickHook = Hook.FromAddress(LogViewer.Addresses.HandleLinkClick.Value, this.HandleLinkClickDetour);
+ this.handleLogModuleUpdate = Hook.FromAddress(RaptureLogModule.Addresses.Update.Value, this.UpdateDetour);
this.printMessageHook.Enable();
this.inventoryItemCopyHook.Enable();
this.handleLinkClickHook.Enable();
+ this.handleLogModuleUpdate.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
@@ -79,6 +84,9 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
///
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
+ ///
+ public event IChatGui.OnLogMessageDelegate? LogMessage;
+
///
public uint LastLinkedItemId { get; private set; }
@@ -110,6 +118,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
this.printMessageHook.Dispose();
this.inventoryItemCopyHook.Dispose();
this.handleLinkClickHook.Dispose();
+ this.handleLogModuleUpdate.Dispose();
}
#region DalamudSeString
@@ -493,6 +502,46 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
Log.Error(ex, "Exception in HandleLinkClickDetour");
}
}
+
+ private void UpdateDetour(RaptureLogModule* thisPtr)
+ {
+ try
+ {
+ foreach (ref var item in thisPtr->LogMessageQueue)
+ {
+ var logMessage = new Chat.LogMessage((LogMessageQueueItem*)Unsafe.AsPointer(ref item));
+
+ // skip any entries that survived the previous Update call as the event was already called for them
+ if (this.seenLogMessageObjects.Contains(logMessage.Address))
+ continue;
+
+ foreach (var action in Delegate.EnumerateInvocationList(this.LogMessage))
+ {
+ try
+ {
+ action(logMessage);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Could not invoke registered OnLogMessageDelegate for {Name}", action.Method);
+ }
+ }
+ }
+
+ this.handleLogModuleUpdate.Original(thisPtr);
+
+ // record the log messages for that we already called the event, but are still in the queue
+ this.seenLogMessageObjects.Clear();
+ foreach (ref var item in thisPtr->LogMessageQueue)
+ {
+ this.seenLogMessageObjects.Add((nint)Unsafe.AsPointer(ref item));
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Exception in UpdateDetour");
+ }
+ }
}
///
@@ -521,6 +570,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
this.chatGuiService.CheckMessageHandled += this.OnCheckMessageForward;
this.chatGuiService.ChatMessageHandled += this.OnMessageHandledForward;
this.chatGuiService.ChatMessageUnhandled += this.OnMessageUnhandledForward;
+ this.chatGuiService.LogMessage += this.OnLogMessageForward;
}
///
@@ -535,6 +585,9 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
///
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
+ ///
+ public event IChatGui.OnLogMessageDelegate? LogMessage;
+
///
public uint LastLinkedItemId => this.chatGuiService.LastLinkedItemId;
@@ -551,11 +604,13 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward;
this.chatGuiService.ChatMessageHandled -= this.OnMessageHandledForward;
this.chatGuiService.ChatMessageUnhandled -= this.OnMessageUnhandledForward;
+ this.chatGuiService.LogMessage -= this.OnLogMessageForward;
this.ChatMessage = null;
this.CheckMessageHandled = null;
this.ChatMessageHandled = null;
this.ChatMessageUnhandled = null;
+ this.LogMessage = null;
}
///
@@ -609,4 +664,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
private void OnMessageUnhandledForward(XivChatType type, int timestamp, SeString sender, SeString message)
=> this.ChatMessageUnhandled?.Invoke(type, timestamp, sender, message);
+
+ private void OnLogMessageForward(Chat.ILogMessage message)
+ => this.LogMessage?.Invoke(message);
}
diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs
index d16b3b9b3..58b9011e6 100644
--- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs
+++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs
@@ -845,7 +845,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
using var rssb = new RentedSeStringBuilder();
var sb = rssb.Builder;
- sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language));
+ sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language)); // appends colortype and edgecolortype
if (!skipLink)
sb.PushLink(LinkMacroPayloadType.Item, itemId, rarity, 0u); // arg3 = some LogMessage flag based on LogKind RowId? => "89 5C 24 20 E8 ?? ?? ?? ?? 48 8B 1F"
@@ -868,6 +868,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!skipLink)
sb.PopLink();
+ sb.PopEdgeColorType();
+ sb.PopColorType();
+
text = sb.ToReadOnlySeString();
}
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 9d75b6aaf..a1954524a 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -32,6 +32,7 @@ using Dalamud.Interface.Windowing;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
+using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
@@ -502,6 +503,34 @@ internal partial class InterfaceManager : IInternalDisposableService
ImGuiHelpers.ClearStacksOnContext();
}
+ ///
+ /// Applies immersive dark mode to the game window based on the current system theme setting.
+ ///
+ internal void SetImmersiveModeFromSystemTheme()
+ {
+ bool useDark = this.IsSystemInDarkMode();
+ this.SetImmersiveMode(useDark);
+ }
+
+ ///
+ /// Checks whether the system use dark mode.
+ ///
+ /// Returns true if dark mode is preferred.
+ internal bool IsSystemInDarkMode()
+ {
+ try
+ {
+ using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(
+ @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
+ var value = key?.GetValue("AppsUseLightTheme") as int?;
+ return value != 1;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
///
/// Toggle Windows 11 immersive mode on the game window.
///
@@ -745,6 +774,18 @@ internal partial class InterfaceManager : IInternalDisposableService
private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args)
{
+ if (args.Message == WM.WM_SETTINGCHANGE)
+ {
+ if (this.dalamudConfiguration.WindowIsImmersive)
+ {
+ if (MemoryHelper.EqualsZeroTerminatedWideString("ImmersiveColorSet", args.LParam) ||
+ MemoryHelper.EqualsZeroTerminatedWideString("VisualStyleChanged", args.LParam))
+ {
+ this.SetImmersiveModeFromSystemTheme();
+ }
+ }
+ }
+
var r = this.backend?.ProcessWndProcW(args.Hwnd, args.Message, args.WParam, args.LParam);
if (r is not null)
args.SuppressWithValue(r.Value);
@@ -859,7 +900,7 @@ internal partial class InterfaceManager : IInternalDisposableService
{
// Requires that game window to be there, which will be the case once game swap chain is initialized.
if (Service.Get().WindowIsImmersive)
- this.SetImmersiveMode(true);
+ this.SetImmersiveModeFromSystemTheme();
}
catch (Exception ex)
{
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs
index b5899e15f..ed9ed2150 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/AddonTree.AtkValues.cs
@@ -27,7 +27,6 @@ public unsafe partial class AddonTree
if (tree.Success)
{
using var tbl = ImRaii.Table("atkUnitBase_atkValueTable"u8, 3, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
-
if (tbl.Success)
{
ImGui.TableSetupColumn("Index"u8);
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
index e094c6d7a..ed1926ce9 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/Events.cs
@@ -29,11 +29,9 @@ public static class Events
}
using var tree = ImRaii.TreeNode($"Events##{(nint)node:X}eventTree");
-
if (tree.Success)
{
using var tbl = ImRaii.Table($"##{(nint)node:X}eventTable", 7, Resizable | SizingFixedFit | Borders | RowBg);
-
if (tbl.Success)
{
ImGui.TableSetupColumn("#"u8, WidthFixed);
@@ -51,18 +49,25 @@ public static class Events
{
ImGui.TableNextColumn();
ImGui.Text($"{i++}");
+
ImGui.TableNextColumn();
ImGui.Text($"{evt->State.EventType}");
+
ImGui.TableNextColumn();
ImGui.Text($"{evt->Param}");
+
ImGui.TableNextColumn();
ImGui.Text($"{evt->State.StateFlags}");
+
ImGui.TableNextColumn();
ImGui.Text($"{evt->State.ReturnFlags}");
+
ImGui.TableNextColumn();
ImGuiHelpers.ClickToCopyText($"{(nint)evt->Target:X}", default, new Vector4(0.6f, 0.6f, 0.6f, 1));
+
ImGui.TableNextColumn();
ImGuiHelpers.ClickToCopyText($"{(nint)evt->Listener:X}", default, new Vector4(0.6f, 0.6f, 0.6f, 1));
+
evt = evt->NextEvent;
}
}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs
index 9a51f1771..13d559c11 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs
@@ -4,6 +4,8 @@ using Dalamud.Bindings.ImGui;
using FFXIVClientStructs.FFXIV.Component.GUI;
+using Lumina.Text.ReadOnly;
+
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Utility.Util;
using static FFXIVClientStructs.FFXIV.Component.GUI.ComponentType;
@@ -90,14 +92,14 @@ internal unsafe class ComponentNodeTree : ResNodeTree
{
case TextInput:
var textInputComponent = (AtkComponentTextInput*)this.Component;
- ImGui.Text($"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.EvaluatedString.StringPtr))}");
- ImGui.Text($"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.RawString.StringPtr))}");
+ ImGui.Text($"InputBase Text1 (Lumina): {new ReadOnlySeStringSpan(textInputComponent->AtkComponentInputBase.EvaluatedString.AsSpan()).ToMacroString()}");
+ ImGui.Text($"InputBase Text2 (Lumina): {new ReadOnlySeStringSpan(textInputComponent->AtkComponentInputBase.RawString.AsSpan()).ToMacroString()}");
// TODO: Reenable when unknowns have been unprivated / named
- // ImGui.Text($"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}");
- // ImGui.Text($"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}");
- ImGui.Text($"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}");
- ImGui.Text($"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}");
- ImGui.Text($"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}");
+ // ImGui.Text($"Text1: {new ReadOnlySeStringSpan(textInputComponent->UnkText01.AsSpan()).ToMacroString()}");
+ // ImGui.Text($"Text2: {new ReadOnlySeStringSpan(textInputComponent->UnkText02.AsSpan()).ToMacroString()}");
+ ImGui.Text($"AvailableLines: {new ReadOnlySeStringSpan(textInputComponent->AvailableLines.AsSpan()).ToMacroString()}");
+ ImGui.Text($"HighlightedAutoTranslateOptionColorPrefix: {new ReadOnlySeStringSpan(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.AsSpan()).ToMacroString()}");
+ ImGui.Text($"HighlightedAutoTranslateOptionColorSuffix: {new ReadOnlySeStringSpan(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.AsSpan()).ToMacroString()}");
break;
case List:
case TreeList:
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs
index ff40db37a..2b2adbcee 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Counter.cs
@@ -1,5 +1,7 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
+using Lumina.Text.ReadOnly;
+
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Utility.Util;
@@ -30,7 +32,7 @@ internal unsafe partial class CounterNodeTree : ResNodeTree
{
if (!isEditorOpen)
{
- PrintFieldValuePairs(("Text", ((AtkCounterNode*)this.Node)->NodeText.ToString()));
+ PrintFieldValuePairs(("Text", new ReadOnlySeStringSpan(((AtkCounterNode*)this.Node)->NodeText.AsSpan()).ToMacroString()));
}
}
}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs
index 13cad9fd0..d4e8e61ab 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Editor.cs
@@ -8,6 +8,8 @@ using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Component.GUI;
+using Lumina.Text.ReadOnly;
+
using static Dalamud.Bindings.ImGui.ImGuiColorEditFlags;
using static Dalamud.Bindings.ImGui.ImGuiInputTextFlags;
using static Dalamud.Bindings.ImGui.ImGuiTableColumnFlags;
@@ -28,10 +30,10 @@ internal unsafe partial class ResNodeTree
private protected void DrawNodeEditorTable()
{
using var tbl = ImRaii.Table($"###Editor{(nint)this.Node}", 2, SizingStretchProp | NoHostExtendX);
- if (tbl.Success)
- {
- this.DrawEditorRows();
- }
+ if (!tbl.Success)
+ return;
+
+ this.DrawEditorRows();
}
///
@@ -60,7 +62,7 @@ internal unsafe partial class ResNodeTree
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
- if (ImGui.DragFloat2($"##{(nint)this.Node:X}position", ref pos, 1, default, default, "%.0f"))
+ if (ImGui.DragFloat2($"##{(nint)this.Node:X}position", ref pos, 1, 0, 0, "%.0f"))
{
this.Node->X = pos.X;
this.Node->Y = pos.Y;
@@ -74,7 +76,7 @@ internal unsafe partial class ResNodeTree
ImGui.Text("Size:"u8);
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
- if (ImGui.DragFloat2($"##{(nint)this.Node:X}size", ref size, 1, 0, default, "%.0f"))
+ if (ImGui.DragFloat2($"##{(nint)this.Node:X}size", ref size, 1, 0, 0, "%.0f"))
{
this.Node->Width = (ushort)Math.Max(size.X, 0);
this.Node->Height = (ushort)Math.Max(size.Y, 0);
@@ -102,7 +104,7 @@ internal unsafe partial class ResNodeTree
ImGui.Text("Origin:"u8);
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(150);
- if (ImGui.DragFloat2($"##{(nint)this.Node:X}origin", ref origin, 1, default, default, "%.0f"))
+ if (ImGui.DragFloat2($"##{(nint)this.Node:X}origin", ref origin, 1, 0, 0, "%.0f"))
{
this.Node->OriginX = origin.X;
this.Node->OriginY = origin.Y;
@@ -121,7 +123,7 @@ internal unsafe partial class ResNodeTree
angle -= 360;
}
- if (ImGui.DragFloat($"##{(nint)this.Node:X}rotation", ref angle, 0.05f, default, default, "%.2f°"))
+ if (ImGui.DragFloat($"##{(nint)this.Node:X}rotation", ref angle, 0.05f, 0, 0, "%.2f°"))
{
this.Node->Rotation = (float)(angle / (180 / Math.PI));
this.Node->DrawFlags |= 0xD;
@@ -169,7 +171,6 @@ internal unsafe partial class ResNodeTree
ImGui.Text("Add:"u8);
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(124);
-
if (ImGui.DragFloat3($"##{(nint)this.Node:X}addRGB", ref add, 1, -255, 255, "%.0f"))
{
this.Node->AddRed = (short)add.X;
@@ -200,7 +201,7 @@ internal unsafe partial class CounterNodeTree
{
base.DrawEditorRows();
- var str = this.CntNode->NodeText.ToString();
+ var str = new ReadOnlySeStringSpan(this.CntNode->NodeText.AsSpan()).ToMacroString();
ImGui.TableNextRow();
ImGui.TableNextColumn();
@@ -300,7 +301,7 @@ internal unsafe partial class TextNodeTree
{
base.DrawEditorRows();
- var text = this.TxtNode->NodeText.ToString();
+ var text = new ReadOnlySeStringSpan(this.TxtNode->NodeText.AsSpan()).ToMacroString();
var fontIndex = FontList.IndexOf(this.TxtNode->FontType);
int fontSize = this.TxtNode->FontSize;
var alignment = this.TxtNode->AlignmentType;
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
index aa7ee9a63..45dd63b53 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Image.cs
@@ -64,7 +64,6 @@ internal unsafe partial class ImageNodeTree : ResNodeTree
}
using var tree = ImRaii.TreeNode($"Texture##texture{(nint)this.TexData.Texture->D3D11ShaderResourceView:X}", SpanFullWidth);
-
if (tree.Success)
{
PrintFieldValuePairs(
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs
index 6844c8a6f..1c06dfb40 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.NineGrid.cs
@@ -60,10 +60,10 @@ internal unsafe partial class NineGridNodeTree : ImageNodeTree
var ngCol = RgbaVector4ToUint(col with { W = 0.75f * col.W });
- ImGui.GetWindowDrawList()
- .AddRect(partBegin, partEnd, RgbaVector4ToUint(col));
- ImGui.GetWindowDrawList().AddRect(ngBegin1, ngEnd1, ngCol);
- ImGui.GetWindowDrawList().AddRect(ngBegin2, ngEnd2, ngCol);
+ var windowDrawList = ImGui.GetWindowDrawList();
+ windowDrawList.AddRect(partBegin, partEnd, RgbaVector4ToUint(col));
+ windowDrawList.AddRect(ngBegin1, ngEnd1, ngCol);
+ windowDrawList.AddRect(ngBegin2, ngEnd2, ngCol);
ImGui.SetCursorPos(cursorLocalPos + uv + new Vector2(0, -20));
ImGui.TextColored(col, $"[#{partId}]\t{part.U}, {part.V}\t{part.Width}x{part.Height}");
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
index f38bef400..086a41efc 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs
@@ -138,7 +138,6 @@ internal unsafe partial class ResNodeTree : IDisposable
PrintNodeList(nodeList, count, addonTree);
var lineEnd = lineStart with { Y = ImGui.GetCursorScreenPos().Y - 7 };
-
if (lineStart.Y < lineEnd.Y)
{
ImGui.GetWindowDrawList().AddLine(lineStart, lineEnd, RgbaVector4ToUint(color), 1);
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
index 618517e62..7ae0d8fca 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Text.cs
@@ -2,16 +2,15 @@ using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Bindings.ImGui;
-using Dalamud.Game.Text.SeStringHandling;
-using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiSeStringRenderer;
-using Dalamud.Interface.Internal.UiDebug2.Utility;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Component.GUI;
+using Lumina.Text.ReadOnly;
+
using static Dalamud.Interface.ColorHelpers;
using static Dalamud.Interface.Internal.UiDebug2.Utility.Gui;
using static Dalamud.Utility.Util;
@@ -65,7 +64,7 @@ internal unsafe partial class TextNodeTree : ResNodeTree
}
catch
{
- ImGui.Text(Marshal.PtrToStringAnsi(new(this.NodeText.StringPtr)) ?? string.Empty);
+ ImGui.Text(new ReadOnlySeStringSpan(this.NodeText.AsSpan()).ToMacroString());
}
PrintFieldValuePairs(
@@ -83,36 +82,24 @@ internal unsafe partial class TextNodeTree : ResNodeTree
private void PrintPayloads()
{
using var tree = ImRaii.TreeNode($"Text Payloads##{(nint)this.Node:X}");
-
if (tree.Success)
{
- var utf8String = this.NodeText;
- var seStringBytes = new byte[utf8String.BufUsed];
- for (var i = 0L; i < utf8String.BufUsed; i++)
+ var idx = 0;
+ foreach (var payload in new ReadOnlySeString(this.NodeText.AsSpan()))
{
- seStringBytes[i] = utf8String.StringPtr.Value[i];
- }
-
- var seString = SeString.Parse(seStringBytes);
- for (var i = 0; i < seString.Payloads.Count; i++)
- {
- var payload = seString.Payloads[i];
- ImGui.Text($"[{i}]");
+ ImGui.Text($"[{idx}]");
ImGui.SameLine();
switch (payload.Type)
{
- case PayloadType.RawText when payload is TextPayload tp:
- {
- Gui.PrintFieldValuePair("Raw Text", tp.Text ?? string.Empty);
+ case ReadOnlySePayloadType.Text:
+ PrintFieldValuePair("Raw Text", payload.ToString());
break;
- }
-
default:
- {
ImGui.Text(payload.ToString());
break;
- }
}
+
+ idx++;
}
}
}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
index af0e0f284..10d3d9362 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs
@@ -58,7 +58,6 @@ public readonly unsafe partial struct TimelineTree
if (animationCount > 0)
{
using var tree = ImRaii.TreeNode($"Timeline##{(nint)this.node:X}timeline", SpanFullWidth);
-
if (tree.Success)
{
PrintFieldValuePair("Timeline", $"{(nint)this.NodeTimeline:X}");
@@ -90,7 +89,6 @@ public readonly unsafe partial struct TimelineTree
if (labelSetCount > 0 && this.Resource->LabelSets is not null)
{
using var tree = ImRaii.TreeNode($"Timeline Label Sets##{(nint)this.node:X}LabelSets", SpanFullWidth);
-
if (tree.Success)
{
this.DrawLabelSets();
@@ -325,7 +323,6 @@ public readonly unsafe partial struct TimelineTree
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 0.65F, 0.4F, 1), isActive))
{
using var tree = ImRaii.TreeNode($"[#{a}] [Frames {animation.StartFrameIdx}-{animation.EndFrameIdx}] {(isActive ? " (Active)" : string.Empty)}###{(nint)this.node}animTree{a}");
-
if (tree.Success)
{
PrintFieldValuePair("Animation", $"{address:X}");
@@ -335,7 +332,6 @@ public readonly unsafe partial struct TimelineTree
if (columns.Count > 0)
{
using var tbl = ImRaii.Table($"##{(nint)this.node}animTable{a}", columns.Count, Borders | SizingFixedFit | RowBg | NoHostExtendX);
-
if (tbl.Success)
{
foreach (var c in columns)
diff --git a/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs b/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
index 3286df1b3..46e0c1f8f 100644
--- a/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/ElementSelector.cs
@@ -160,65 +160,61 @@ internal unsafe class ElementSelector : IDisposable
if (ch.Success)
{
using var gr = ImRaii.Group();
- if (gr.Success)
+ Gui.PrintFieldValuePair("Mouse Position", $"{mousePos.X}, {mousePos.Y}");
+ ImGui.Spacing();
+ ImGui.Text("RESULTS:\n"u8);
+
+ var i = 0;
+ foreach (var a in addonResults)
{
- Gui.PrintFieldValuePair("Mouse Position", $"{mousePos.X}, {mousePos.Y}");
- ImGui.Spacing();
- ImGui.Text("RESULTS:\n"u8);
+ var name = a.Addon->NameString;
+ ImGui.Text($"[Addon] {name}");
- var i = 0;
- foreach (var a in addonResults)
+ using var indent = ImRaii.PushIndent(15.0f);
+ foreach (var n in a.Nodes)
{
- var name = a.Addon->NameString;
- ImGui.Text($"[Addon] {name}");
- ImGui.Indent(15);
- foreach (var n in a.Nodes)
+ var nSelected = i++ == this.index;
+
+ PrintNodeHeaderOnly(n.Node, nSelected, a.Addon);
+
+ if (nSelected && ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
- var nSelected = i++ == this.index;
+ this.Active = false;
- PrintNodeHeaderOnly(n.Node, nSelected, a.Addon);
+ this.uiDebug2.SelectedAddonName = a.Addon->NameString;
- if (nSelected && ImGui.IsMouseClicked(ImGuiMouseButton.Left))
+ var ptrList = new List { (nint)n.Node };
+
+ var nextNode = n.Node->ParentNode;
+ while (nextNode != null)
{
- this.Active = false;
-
- this.uiDebug2.SelectedAddonName = a.Addon->NameString;
-
- var ptrList = new List { (nint)n.Node };
-
- var nextNode = n.Node->ParentNode;
- while (nextNode != null)
- {
- ptrList.Add((nint)nextNode);
- nextNode = nextNode->ParentNode;
- }
-
- SearchResults = [.. ptrList];
- Countdown = 100;
- Scrolled = false;
+ ptrList.Add((nint)nextNode);
+ nextNode = nextNode->ParentNode;
}
- if (nSelected)
- {
- n.NodeBounds.DrawFilled(new(1, 1, 0.2f, 1));
- }
+ SearchResults = [.. ptrList];
+ Countdown = 100;
+ Scrolled = false;
}
- ImGui.Indent(-15);
+ if (nSelected)
+ {
+ n.NodeBounds.DrawFilled(new(1, 1, 0.2f, 1));
+ }
+ }
+ }
+
+ if (i != 0)
+ {
+ this.index -= (int)ImGui.GetIO().MouseWheel;
+ while (this.index < 0)
+ {
+ this.index += i;
}
- if (i != 0)
+ while (this.index >= i)
{
- this.index -= (int)ImGui.GetIO().MouseWheel;
- while (this.index < 0)
- {
- this.index += i;
- }
-
- while (this.index >= i)
- {
- this.index -= i;
- }
+ this.index -= i;
}
}
}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs b/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
index 4684caa60..69fbc17fb 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Popout.Addon.cs
@@ -39,7 +39,7 @@ internal class AddonPopoutWindow : Window, IDisposable
///
public override void Draw()
{
- using var ch = ImRaii.Child($"{this.WindowName}child", new(-1, -1), true);
+ using var ch = ImRaii.Child($"{this.WindowName}child", Vector2.Zero, true);
if (ch.Success)
{
this.addonTree.Draw();
diff --git a/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs b/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
index 88af38531..de476983f 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Popout.Node.cs
@@ -39,7 +39,7 @@ internal unsafe class NodePopoutWindow : Window, IDisposable
this.PositionCondition = ImGuiCond.Once;
this.SizeCondition = ImGuiCond.Once;
this.Size = new(700, 200);
- this.SizeConstraints = new() { MinimumSize = new(100, 100) };
+ this.SizeConstraints = new() { MinimumSize = new Vector2(100, 100) };
}
private AddonTree AddonTree => this.resNodeTree.AddonTree;
@@ -51,7 +51,7 @@ internal unsafe class NodePopoutWindow : Window, IDisposable
{
if (this.Node != null && this.AddonTree.ContainsNode(this.Node))
{
- using var ch = ImRaii.Child($"{(nint)this.Node:X}popoutChild", new(-1, -1), true);
+ using var ch = ImRaii.Child($"{(nint)this.Node:X}popoutChild", Vector2.Zero, true);
if (ch.Success)
{
ResNodeTree.GetOrCreate(this.Node, this.AddonTree).Print(null, this.firstDraw);
diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs b/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
index 5c1e72aed..adfbfa81c 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Utility/Gui.cs
@@ -105,12 +105,8 @@ internal static class Gui
var index = (int)Math.Floor(prog * tooltips.Length);
- using var tt = ImRaii.Tooltip();
-
- if (tt.Success)
- {
- ImGui.Text(tooltips[index]);
- }
+ using var tooltip = ImRaii.Tooltip();
+ ImGui.Text(tooltips[index]);
return true;
}
@@ -124,13 +120,14 @@ internal static class Gui
{
if ((mask & 0b10) > 0)
{
- ImGui.Dummy(new(padding * ImGui.GetIO().FontGlobalScale));
+ ImGuiHelpers.ScaledDummy(padding);
}
ImGui.Separator();
+
if ((mask & 0b01) > 0)
{
- ImGui.Dummy(new(padding * ImGui.GetIO().FontGlobalScale));
+ ImGuiHelpers.ScaledDummy(padding);
}
}
}
diff --git a/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs b/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs
index 3af306daf..20feb903f 100644
--- a/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs
+++ b/Dalamud/Interface/Internal/UiDebug2/Utility/NodeBounds.cs
@@ -62,10 +62,11 @@ public unsafe struct NodeBounds
return;
}
+ var backgroundDrawList = ImGui.GetBackgroundDrawList();
if (this.Points.Count == 1)
{
- ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, RgbaVector4ToUint(col with { W = col.W / 2 }), 12, thickness);
- ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], thickness, RgbaVector4ToUint(col), 12, thickness + 1);
+ backgroundDrawList.AddCircle(this.Points[0], 10, RgbaVector4ToUint(col with { W = col.W / 2 }), 12, thickness);
+ backgroundDrawList.AddCircle(this.Points[0], thickness, RgbaVector4ToUint(col), 12, thickness + 1);
}
else
{
@@ -75,8 +76,7 @@ public unsafe struct NodeBounds
path.Add(p);
}
- ImGui.GetBackgroundDrawList()
- .AddPolyline(ref path[0], path.Length, RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness);
+ backgroundDrawList.AddPolyline(ref path[0], path.Length, RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness);
path.Dispose();
}
@@ -94,11 +94,11 @@ public unsafe struct NodeBounds
return;
}
+ var backgroundDrawList = ImGui.GetBackgroundDrawList();
if (this.Points.Count == 1)
{
- ImGui.GetBackgroundDrawList()
- .AddCircleFilled(this.Points[0], 10, RgbaVector4ToUint(col with { W = col.W / 2 }), 12);
- ImGui.GetBackgroundDrawList().AddCircle(this.Points[0], 10, RgbaVector4ToUint(col), 12, thickness);
+ backgroundDrawList.AddCircleFilled(this.Points[0], 10, RgbaVector4ToUint(col with { W = col.W / 2 }), 12);
+ backgroundDrawList.AddCircle(this.Points[0], 10, RgbaVector4ToUint(col), 12, thickness);
}
else
{
@@ -108,10 +108,8 @@ public unsafe struct NodeBounds
path.Add(p);
}
- ImGui.GetBackgroundDrawList()
- .AddConvexPolyFilled(ref path[0], path.Length, RgbaVector4ToUint(col with { W = col.W / 2 }));
- ImGui.GetBackgroundDrawList()
- .AddPolyline(ref path[0], path.Length, RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness);
+ backgroundDrawList.AddConvexPolyFilled(ref path[0], path.Length, RgbaVector4ToUint(col with { W = col.W / 2 }));
+ backgroundDrawList.AddPolyline(ref path[0], path.Length, RgbaVector4ToUint(col), ImDrawFlags.Closed, thickness);
path.Dispose();
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
index dbc778614..444b923ab 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
@@ -45,6 +45,7 @@ internal class DataWindow : Window, IDisposable
new ImGuiWidget(),
new InventoryWidget(),
new KeyStateWidget(),
+ new LogMessageMonitorWidget(),
new MarketBoardWidget(),
new NetworkMonitorWidget(),
new NounProcessorWidget(),
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/LogMessageMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/LogMessageMonitorWidget.cs
new file mode 100644
index 000000000..fde46f0c7
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/LogMessageMonitorWidget.cs
@@ -0,0 +1,156 @@
+using System.Buffers;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+
+using Dalamud.Bindings.ImGui;
+using Dalamud.Game.Chat;
+using Dalamud.Game.Gui;
+using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+
+using Lumina.Text.ReadOnly;
+
+using ImGuiTable = Dalamud.Interface.Utility.ImGuiTable;
+
+namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
+
+///
+/// Widget to display the LogMessages.
+///
+internal class LogMessageMonitorWidget : IDataWindowWidget
+{
+ private readonly ConcurrentQueue messages = new();
+
+ private bool trackMessages;
+ private int trackedMessages;
+ private Regex? filterRegex;
+ private string filterString = string.Empty;
+
+ ///
+ public string[]? CommandShortcuts { get; init; } = ["logmessage"];
+
+ ///
+ public string DisplayName { get; init; } = "LogMessage Monitor";
+
+ ///
+ public bool Ready { get; set; }
+
+ ///
+ public void Load()
+ {
+ this.trackMessages = false;
+ this.trackedMessages = 20;
+ this.filterRegex = null;
+ this.filterString = string.Empty;
+ this.messages.Clear();
+ this.Ready = true;
+ }
+
+ ///
+ public void Draw()
+ {
+ var network = Service.Get();
+ if (ImGui.Checkbox("Track LogMessages"u8, ref this.trackMessages))
+ {
+ if (this.trackMessages)
+ {
+ network.LogMessage += this.OnLogMessage;
+ }
+ else
+ {
+ network.LogMessage -= this.OnLogMessage;
+ }
+ }
+
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2);
+ if (ImGui.DragInt("Stored Number of Messages"u8, ref this.trackedMessages, 0.1f, 1, 512))
+ {
+ this.trackedMessages = Math.Clamp(this.trackedMessages, 1, 512);
+ }
+
+ if (ImGui.Button("Clear Stored Messages"u8))
+ {
+ this.messages.Clear();
+ }
+
+ this.DrawFilterInput();
+
+ ImGuiTable.DrawTable(string.Empty, this.messages.Where(m => this.filterRegex == null || this.filterRegex.IsMatch(m.Formatted.ExtractText())), this.DrawNetworkPacket, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp, "LogMessageId", "Source", "Target", "Parameters", "Formatted");
+ }
+
+ private void DrawNetworkPacket(LogMessageData data)
+ {
+ ImGui.TableNextColumn();
+ ImGui.Text(data.LogMessageId.ToString());
+
+ ImGui.TableNextColumn();
+ ImGuiHelpers.SeStringWrapped(data.Source);
+
+ ImGui.TableNextColumn();
+ ImGuiHelpers.SeStringWrapped(data.Target);
+
+ ImGui.TableNextColumn();
+ ImGui.Text(data.Parameters);
+
+ ImGui.TableNextColumn();
+ ImGuiHelpers.SeStringWrapped(data.Formatted);
+ }
+
+ private void DrawFilterInput()
+ {
+ var invalidRegEx = this.filterString.Length > 0 && this.filterRegex == null;
+ using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx);
+ using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx);
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+ if (!ImGui.InputTextWithHint("##Filter"u8, "Regex Filter..."u8, ref this.filterString, 1024))
+ {
+ return;
+ }
+
+ if (this.filterString.Length == 0)
+ {
+ this.filterRegex = null;
+ }
+ else
+ {
+ try
+ {
+ this.filterRegex = new Regex(this.filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ }
+ catch
+ {
+ this.filterRegex = null;
+ }
+ }
+ }
+
+ private void OnLogMessage(ILogMessage message)
+ {
+ var buffer = new ArrayBufferWriter();
+ var writer = new Utf8JsonWriter(buffer);
+
+ writer.WriteStartArray();
+ for (var i = 0; i < message.ParameterCount; i++)
+ {
+ if (message.TryGetStringParameter(i, out var str))
+ writer.WriteStringValue(str.ExtractText());
+ else if (message.TryGetIntParameter(i, out var num))
+ writer.WriteNumberValue(num);
+ else
+ writer.WriteNullValue();
+ }
+
+ writer.WriteEndArray();
+ writer.Flush();
+
+ this.messages.Enqueue(new LogMessageData(message.LogMessageId, message.SourceEntity?.Name ?? default, message.TargetEntity?.Name ?? default, buffer.WrittenMemory, message.FormatLogMessageForDebugging()));
+ while (this.messages.Count > this.trackedMessages)
+ {
+ this.messages.TryDequeue(out _);
+ }
+ }
+
+ private readonly record struct LogMessageData(uint LogMessageId, ReadOnlySeString Source, ReadOnlySeString Target, ReadOnlyMemory Parameters, ReadOnlySeString Formatted);
+}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
index 7a2631fbf..851957b4b 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
@@ -1,4 +1,5 @@
using Dalamud.Bindings.ImGui;
+using Dalamud.Game.Chat;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
@@ -12,8 +13,12 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps;
internal class ChatSelfTestStep : ISelfTestStep
{
private int step = 0;
- private bool subscribed = false;
- private bool hasPassed = false;
+ private bool subscribedChatMessage = false;
+ private bool subscribedLogMessage = false;
+ private bool hasSeenEchoMessage = false;
+ private bool hasSeenActionMessage = false;
+ private string actionName = string.Empty;
+ private string actionUser = string.Empty;
///
public string Name => "Test Chat";
@@ -34,20 +39,55 @@ internal class ChatSelfTestStep : ISelfTestStep
case 1:
ImGui.Text("Type \"/e DALAMUD\" in chat...");
- if (!this.subscribed)
+ if (!this.subscribedChatMessage)
{
- this.subscribed = true;
+ this.subscribedChatMessage = true;
chatGui.ChatMessage += this.ChatOnOnChatMessage;
}
- if (this.hasPassed)
+ if (this.hasSeenEchoMessage)
{
chatGui.ChatMessage -= this.ChatOnOnChatMessage;
- this.subscribed = false;
- return SelfTestStepResult.Pass;
+ this.subscribedChatMessage = false;
+ this.step++;
}
break;
+
+ case 2:
+ ImGui.Text("Use any action (for example Sprint) or be near a player using an action.");
+
+ if (!this.subscribedLogMessage)
+ {
+ this.subscribedLogMessage = true;
+ chatGui.LogMessage += this.ChatOnLogMessage;
+ }
+
+ if (this.hasSeenActionMessage)
+ {
+ ImGui.Text($"{this.actionUser} used {this.actionName}.");
+ ImGui.Text("Is this correct?");
+
+ if (ImGui.Button("Yes"))
+ {
+ chatGui.LogMessage -= this.ChatOnLogMessage;
+ this.subscribedLogMessage = false;
+ this.step++;
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("No"))
+ {
+ chatGui.LogMessage -= this.ChatOnLogMessage;
+ this.subscribedLogMessage = false;
+ return SelfTestStepResult.Fail;
+ }
+ }
+
+ break;
+
+ default:
+ return SelfTestStepResult.Pass;
}
return SelfTestStepResult.Waiting;
@@ -59,7 +99,9 @@ internal class ChatSelfTestStep : ISelfTestStep
var chatGui = Service.Get();
chatGui.ChatMessage -= this.ChatOnOnChatMessage;
- this.subscribed = false;
+ chatGui.LogMessage -= this.ChatOnLogMessage;
+ this.subscribedChatMessage = false;
+ this.subscribedLogMessage = false;
}
private void ChatOnOnChatMessage(
@@ -67,7 +109,17 @@ internal class ChatSelfTestStep : ISelfTestStep
{
if (type == XivChatType.Echo && message.TextValue == "DALAMUD")
{
- this.hasPassed = true;
+ this.hasSeenEchoMessage = true;
+ }
+ }
+
+ private void ChatOnLogMessage(ILogMessage message)
+ {
+ if (message.LogMessageId == 533 && message.TryGetStringParameter(0, out var value))
+ {
+ this.hasSeenActionMessage = true;
+ this.actionUser = message.SourceEntity?.Name.ExtractText() ?? "";
+ this.actionName = value.ExtractText();
}
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
index f2ca1f53a..d8bf31bfd 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
@@ -66,7 +66,14 @@ internal sealed class SettingsTabLook : SettingsTab
{
try
{
- Service.GetNullable()?.SetImmersiveMode(b);
+ if (b)
+ {
+ Service.GetNullable()?.SetImmersiveModeFromSystemTheme();
+ }
+ else
+ {
+ Service.GetNullable()?.SetImmersiveMode(false);
+ }
}
catch (Exception ex)
{
diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs
index d23b67e4f..5f7f69f2f 100644
--- a/Dalamud/Memory/MemoryHelper.cs
+++ b/Dalamud/Memory/MemoryHelper.cs
@@ -268,6 +268,31 @@ public static unsafe class MemoryHelper
}
}
+ ///
+ /// Compares a UTF-16 character span with a null-terminated UTF-16 string at .
+ ///
+ /// UTF-16 character span (e.g., from a string literal).
+ /// Address of null-terminated UTF-16 (wide) string, as used by Windows APIs.
+ /// if equal; otherwise, .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe bool EqualsZeroTerminatedWideString(
+ scoped ReadOnlySpan charSpan,
+ nint memoryAddress)
+ {
+ if (memoryAddress == 0)
+ return charSpan.Length == 0;
+
+ char* p = (char*)memoryAddress;
+
+ foreach (char c in charSpan)
+ {
+ if (*p++ != c)
+ return false;
+ }
+
+ return *p == '\0';
+ }
+
///
/// Read a UTF-8 encoded string from a specified memory address.
///
diff --git a/Dalamud/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs
index 572ac6c95..2bb7b6913 100644
--- a/Dalamud/Plugin/Services/IChatGui.cs
+++ b/Dalamud/Plugin/Services/IChatGui.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
+using Dalamud.Game.Chat;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
@@ -50,6 +51,12 @@ public interface IChatGui : IDalamudService
/// The message sent.
public delegate void OnMessageUnhandledDelegate(XivChatType type, int timestamp, SeString sender, SeString message);
+ ///
+ /// A delegate type used with the event.
+ ///
+ /// The message sent.
+ public delegate void OnLogMessageDelegate(ILogMessage message);
+
///
/// Event that will be fired when a chat message is sent to chat by the game.
///
@@ -70,6 +77,11 @@ public interface IChatGui : IDalamudService
///
public event OnMessageUnhandledDelegate ChatMessageUnhandled;
+ ///
+ /// Event that will be fired when a log message, that is a chat message based on entries in the LogMessage sheet, is sent.
+ ///
+ public event OnLogMessageDelegate LogMessage;
+
///
/// Gets the ID of the last linked item.
///
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index b6f886afc..d83e0c13d 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit b6f886afc2b1a54d8fd76c37a260a05f214a559e
+Subproject commit d83e0c13d3c802d4a483f373edcd129bc4802073
diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel
index 52cb5e0a9..31e50c3f2 160000
--- a/lib/Lumina.Excel
+++ b/lib/Lumina.Excel
@@ -1 +1 @@
-Subproject commit 52cb5e0a9a7a1138d8c2406c277307a6c9ad8898
+Subproject commit 31e50c3f267dd845891b328140106a0cc3b1f35e