From 282fa87571c10b589c7a0b438db5cadb6d9d8533 Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Mon, 22 Dec 2025 19:44:00 +0100
Subject: [PATCH 01/13] Add event for LogMessages being added to the chat
---
Dalamud/Game/Chat/LogMessage.cs | 220 ++++++++++++++++++++++++++
Dalamud/Game/Chat/LogMessageEntity.cs | 96 +++++++++++
Dalamud/Game/Gui/ChatGui.cs | 58 +++++++
Dalamud/Plugin/Services/IChatGui.cs | 13 ++
4 files changed, 387 insertions(+)
create mode 100644 Dalamud/Game/Chat/LogMessage.cs
create mode 100644 Dalamud/Game/Chat/LogMessageEntity.cs
diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs
new file mode 100644
index 000000000..cf423bd6e
--- /dev/null
+++ b/Dalamud/Game/Chat/LogMessage.cs
@@ -0,0 +1,220 @@
+using Dalamud.Data;
+using Dalamud.Game.Text.SeStringHandling;
+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 System.Diagnostics.CodeAnalysis;
+
+using TerraFX.Interop.Windows;
+
+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, [NotNullWhen(true)] out SeString? 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.
+ SeString FormatLogMessageForDebugging();
+}
+
+///
+/// This struct represents a status effect an actor is afflicted by.
+///
+/// A pointer to the Status.
+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);
+
+ public LogMessageEntity SourceEntity => new LogMessageEntity(ptr, true);
+ ///
+ ILogMessageEntity? ILogMessage.SourceEntity => ptr->SourceKind == EntityRelationKind.None ? null : this.SourceEntity;
+
+ public LogMessageEntity TargetEntity => new LogMessageEntity(ptr, false);
+
+ ///
+ ILogMessageEntity? ILogMessage.TargetEntity => ptr->TargetKind == EntityRelationKind.None ? null : this.TargetEntity;
+
+ ///
+ public int ParameterCount => ptr->Parameters.Count;
+
+ public bool TryGetParameter(int index, out TextParameter value)
+ {
+ if (index < 0 || index >= ptr->Parameters.Count)
+ {
+ value = default;
+ return false;
+ }
+
+ value = ptr->Parameters[index];
+ return true;
+ }
+
+ ///
+ 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, [NotNullWhen(true)] out SeString? value)
+ {
+ value = null;
+ if (!this.TryGetParameter(index, out var parameter)) return false;
+ if (parameter.Type == TextParameterType.String)
+ {
+ value = SeString.Parse(parameter.StringValue.Value);
+ return true;
+ }
+ if (parameter.Type == TextParameterType.ReferencedUtf8String)
+ {
+ value = SeString.Parse(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan());
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ public SeString FormatLogMessageForDebugging()
+ {
+ var logModule = RaptureLogModule.Instance();
+
+ // the formatting logic is taken from RaptureLogModule_Update
+
+ var utf8 = new Utf8String();
+ SetName(logModule, this.SourceEntity);
+ SetName(logModule, this.TargetEntity);
+ logModule->RaptureTextModule->FormatString(this.GameData.Value.Text.ToDalamudString().EncodeWithNullTerminator(), &ptr->Parameters, &utf8);
+
+ return SeString.Parse(utf8.AsSpan());
+
+ 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);
+ }
+ }
+ }
+
+
+ public static bool operator ==(LogMessage x, LogMessage y) => x.Equals(y);
+
+ public static bool operator !=(LogMessage x, LogMessage y) => !(x == y);
+
+ public bool Equals(LogMessage other)
+ {
+ return this.LogMessageId == other.LogMessageId && this.SourceEntity == other.SourceEntity && this.TargetEntity == other.TargetEntity;
+ }
+
+ ///
+ 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);
+ }
+}
diff --git a/Dalamud/Game/Chat/LogMessageEntity.cs b/Dalamud/Game/Chat/LogMessageEntity.cs
new file mode 100644
index 000000000..e4c81c16a
--- /dev/null
+++ b/Dalamud/Game/Chat/LogMessageEntity.cs
@@ -0,0 +1,96 @@
+using System.Diagnostics.CodeAnalysis;
+
+using Dalamud.Data;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Plugin.Services;
+
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+
+using Lumina.Excel;
+using Lumina.Excel.Sheets;
+
+namespace Dalamud.Game.Chat;
+
+///
+/// Interface representing an entity related to a log message.
+///
+public interface ILogMessageEntity : IEquatable
+{
+ ///
+ /// Gets the name of this entity.
+ ///
+ SeString 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 boolean indicating if this entity is a player.
+ ///
+ bool IsPlayer { get; }
+}
+
+
+///
+/// This struct represents a status effect an actor is afflicted by.
+///
+/// A pointer to the Status.
+internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool source) : ILogMessageEntity
+{
+ public Span NameSpan => source ? ptr->SourceName : ptr->TargetName;
+
+ public SeString Name => SeString.Parse(this.NameSpan);
+
+ public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld;
+
+ public RowRef HomeWorld => LuminaUtils.CreateRef(this.HomeWorldId);
+
+ public uint ObjStrId => source ? ptr->SourceObjStrId : ptr->TargetObjStrId;
+
+ public byte Kind => source ? (byte)ptr->SourceKind : (byte)ptr->TargetKind;
+
+ public byte Sex => source ? ptr->SourceSex : ptr->TargetSex;
+
+ public bool IsPlayer => source ? ptr->SourceIsPlayer : ptr->TargetIsPlayer;
+
+ public 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(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;
+ }
+
+ ///
+ 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);
+ }
+}
diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs
index 30e2b676c..5208c019b 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 = new();
+ private readonly List seenLogMessageObjects = new();
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((IntPtr)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/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs
index 572ac6c95..eec25cb5a 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,13 @@ 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 +78,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.
///
From b2397efb25075b0b939f5eb5def9cde86b6e971a Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Mon, 22 Dec 2025 20:21:07 +0100
Subject: [PATCH 02/13] Add Self Test
---
.../SelfTest/Steps/ChatSelfTestStep.cs | 78 +++++++++++++++++--
1 file changed, 72 insertions(+), 6 deletions(-)
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
index 7a2631fbf..16fd3b01e 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 subscribedChatMessage = false;
+ private bool subscribedLogMessage = false;
private bool hasPassed = false;
+ private bool hasTeleportGil = false;
+ private bool hasTeleportTicket = false;
+ private int teleportCount = 0;
///
public string Name => "Test Chat";
@@ -34,20 +39,63 @@ 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)
{
chatGui.ChatMessage -= this.ChatOnOnChatMessage;
- this.subscribed = false;
- return SelfTestStepResult.Pass;
+ this.subscribedChatMessage = false;
+ this.step++;
}
break;
+
+ case 2:
+ ImGui.Text("Teleport somewhere...");
+
+ if (!this.subscribedLogMessage)
+ {
+ this.subscribedLogMessage = true;
+ chatGui.LogMessage += this.ChatOnLogMessage;
+ }
+
+ if (this.hasTeleportGil)
+ {
+ ImGui.Text($"You spent {this.teleportCount} gil to teleport.");
+ }
+ if (this.hasTeleportTicket)
+ {
+ ImGui.Text($"You used a ticket to teleport and have {this.teleportCount} remaining.");
+ }
+
+ if (this.hasTeleportGil || this.hasTeleportTicket)
+ {
+ 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 +107,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(
@@ -70,4 +120,20 @@ internal class ChatSelfTestStep : ISelfTestStep
this.hasPassed = true;
}
}
+
+ private void ChatOnLogMessage(ILogMessage message)
+ {
+ if (message.LogMessageId == 4590 && message.TryGetIntParameter(0, out var value))
+ {
+ this.hasTeleportGil = true;
+ this.hasTeleportTicket = false;
+ this.teleportCount = value;
+ }
+ if (message.LogMessageId == 4591 && message.TryGetIntParameter(0, out var item) && item == 7569 && message.TryGetIntParameter(1, out var remaining))
+ {
+ this.hasTeleportGil = false;
+ this.hasTeleportTicket = true;
+ this.teleportCount = remaining;
+ }
+ }
}
From 3aca09d0fb7fa94c769a898b68d96f7d6e630983 Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Mon, 22 Dec 2025 22:28:44 +0100
Subject: [PATCH 03/13] review
---
Dalamud/Game/Chat/LogMessage.cs | 10 ++++++----
Dalamud/Game/Gui/ChatGui.cs | 4 ++--
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs
index cf423bd6e..92217e1c6 100644
--- a/Dalamud/Game/Chat/LogMessage.cs
+++ b/Dalamud/Game/Chat/LogMessage.cs
@@ -12,7 +12,7 @@ using Lumina.Excel;
using System.Diagnostics.CodeAnalysis;
-using TerraFX.Interop.Windows;
+using Lumina.Text.ReadOnly;
namespace Dalamud.Game.Chat;
@@ -72,7 +72,7 @@ public interface ILogMessage : IEquatable
///
/// This can cause side effects such as playing sound effects and thus should only be used for debugging.
/// The formatted string.
- SeString FormatLogMessageForDebugging();
+ ReadOnlySeString FormatLogMessageForDebugging();
}
///
@@ -144,7 +144,7 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa
}
///
- public SeString FormatLogMessageForDebugging()
+ public ReadOnlySeString FormatLogMessageForDebugging()
{
var logModule = RaptureLogModule.Instance();
@@ -155,7 +155,9 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa
SetName(logModule, this.TargetEntity);
logModule->RaptureTextModule->FormatString(this.GameData.Value.Text.ToDalamudString().EncodeWithNullTerminator(), &ptr->Parameters, &utf8);
- return SeString.Parse(utf8.AsSpan());
+ var result = new ReadOnlySeString(utf8.AsSpan());
+ utf8.Dtor();
+ return result;
void SetName(RaptureLogModule* self, LogMessageEntity item)
{
diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs
index 5208c019b..c6405fb35 100644
--- a/Dalamud/Game/Gui/ChatGui.cs
+++ b/Dalamud/Game/Gui/ChatGui.cs
@@ -42,7 +42,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
private readonly Queue chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action> dalamudLinkHandlers = new();
- private readonly List seenLogMessageObjects = new();
+ private readonly List seenLogMessageObjects = new();
private readonly Hook printMessageHook;
private readonly Hook inventoryItemCopyHook;
@@ -534,7 +534,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
this.seenLogMessageObjects.Clear();
foreach (ref var item in thisPtr->LogMessageQueue)
{
- this.seenLogMessageObjects.Add((IntPtr)Unsafe.AsPointer(ref item));
+ this.seenLogMessageObjects.Add((nint)Unsafe.AsPointer(ref item));
}
}
catch (Exception ex)
From 9da178ad569f6ff0809e1c6e023b54b962521b0e Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Tue, 23 Dec 2025 12:34:04 +0100
Subject: [PATCH 04/13] review (2)
---
Dalamud/Game/Chat/LogMessage.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs
index 92217e1c6..fbe3dec3f 100644
--- a/Dalamud/Game/Chat/LogMessage.cs
+++ b/Dalamud/Game/Chat/LogMessage.cs
@@ -150,14 +150,12 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa
// the formatting logic is taken from RaptureLogModule_Update
- var utf8 = new Utf8String();
+ using var utf8 = new Utf8String();
SetName(logModule, this.SourceEntity);
SetName(logModule, this.TargetEntity);
logModule->RaptureTextModule->FormatString(this.GameData.Value.Text.ToDalamudString().EncodeWithNullTerminator(), &ptr->Parameters, &utf8);
- var result = new ReadOnlySeString(utf8.AsSpan());
- utf8.Dtor();
- return result;
+ return new ReadOnlySeString(utf8.AsSpan());
void SetName(RaptureLogModule* self, LogMessageEntity item)
{
From f76d77f79d09972d07cfd8aa030f2a560af17dc1 Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Tue, 23 Dec 2025 12:41:46 +0100
Subject: [PATCH 05/13] rewview (3) and fix some copy docs comments
---
Dalamud/Game/Chat/LogMessage.cs | 4 ++--
Dalamud/Game/Chat/LogMessageEntity.cs | 11 ++++++-----
2 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs
index fbe3dec3f..931b3fb3a 100644
--- a/Dalamud/Game/Chat/LogMessage.cs
+++ b/Dalamud/Game/Chat/LogMessage.cs
@@ -76,9 +76,9 @@ public interface ILogMessage : IEquatable
}
///
-/// This struct represents a status effect an actor is afflicted by.
+/// This struct represents log message in the queue to be added to the chat.
///
-/// A pointer to the Status.
+/// A pointer to the log message.
internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessage
{
///
diff --git a/Dalamud/Game/Chat/LogMessageEntity.cs b/Dalamud/Game/Chat/LogMessageEntity.cs
index e4c81c16a..b465dc158 100644
--- a/Dalamud/Game/Chat/LogMessageEntity.cs
+++ b/Dalamud/Game/Chat/LogMessageEntity.cs
@@ -1,13 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data;
-using Dalamud.Game.Text.SeStringHandling;
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;
@@ -19,7 +19,7 @@ public interface ILogMessageEntity : IEquatable
///
/// Gets the name of this entity.
///
- SeString Name { get; }
+ ReadOnlySeString Name { get; }
///
/// Gets the ID of the homeworld of this entity, if it is a player.
@@ -44,14 +44,15 @@ public interface ILogMessageEntity : IEquatable
///
-/// This struct represents a status effect an actor is afflicted by.
+/// This struct represents an entity related to a log message.
///
-/// A pointer to the Status.
+/// 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 Span NameSpan => source ? ptr->SourceName : ptr->TargetName;
- public SeString Name => SeString.Parse(this.NameSpan);
+ public ReadOnlySeString Name => new ReadOnlySeString(this.NameSpan);
public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld;
From 186b1b83765cd17d58fd70dc6c53a26af70b85ff Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Tue, 23 Dec 2025 14:58:47 +0100
Subject: [PATCH 06/13] Use SeStringEvaluator instead of RaptureTextModule for
the debug display
---
Dalamud/Game/Chat/LogMessage.cs | 13 +++++--------
Dalamud/Game/Text/Evaluator/SeStringParameter.cs | 10 ++++++++++
2 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs
index 931b3fb3a..37b341428 100644
--- a/Dalamud/Game/Chat/LogMessage.cs
+++ b/Dalamud/Game/Chat/LogMessage.cs
@@ -1,18 +1,18 @@
using Dalamud.Data;
+using Dalamud.Game.Text.Evaluator;
using Dalamud.Game.Text.SeStringHandling;
-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;
using System.Diagnostics.CodeAnalysis;
-
-using Lumina.Text.ReadOnly;
+using System.Linq;
namespace Dalamud.Game.Chat;
@@ -150,12 +150,9 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa
// the formatting logic is taken from RaptureLogModule_Update
- using var utf8 = new Utf8String();
SetName(logModule, this.SourceEntity);
SetName(logModule, this.TargetEntity);
- logModule->RaptureTextModule->FormatString(this.GameData.Value.Text.ToDalamudString().EncodeWithNullTerminator(), &ptr->Parameters, &utf8);
-
- return new ReadOnlySeString(utf8.AsSpan());
+ return Service.Get().EvaluateFromLogMessage(this.LogMessageId, ptr->Parameters.Select(p => (SeStringParameter)p).ToArray());
void SetName(RaptureLogModule* self, LogMessageEntity item)
{
diff --git a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
index 036d1c921..a8fe3b3b9 100644
--- a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
+++ b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
@@ -1,5 +1,7 @@
using System.Globalization;
+using FFXIVClientStructs.FFXIV.Component.Text;
+
using Lumina.Text.ReadOnly;
using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
@@ -75,4 +77,12 @@ public readonly struct SeStringParameter
public static implicit operator SeStringParameter(string value) => new(value);
public static implicit operator SeStringParameter(ReadOnlySpan value) => new(value);
+
+ public static unsafe implicit operator SeStringParameter(TextParameter value) => value.Type switch
+ {
+ TextParameterType.Uninitialized => default,
+ TextParameterType.Integer => new((uint)value.IntValue),
+ TextParameterType.ReferencedUtf8String => new(new ReadOnlySeString(value.ReferencedUtf8StringValue->Utf8String.AsSpan())),
+ TextParameterType.String => new(value.StringValue),
+ };
}
From bf75937cc05d542504039d375efdf477d6f2e435 Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Tue, 23 Dec 2025 21:01:29 +0100
Subject: [PATCH 07/13] Don't go through SeString to null terminate a string
---
Dalamud/Game/Chat/LogMessage.cs | 12 ++++++++----
Dalamud/Game/Text/Evaluator/SeStringParameter.cs | 10 ----------
2 files changed, 8 insertions(+), 14 deletions(-)
diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs
index 37b341428..5d59a84ee 100644
--- a/Dalamud/Game/Chat/LogMessage.cs
+++ b/Dalamud/Game/Chat/LogMessage.cs
@@ -1,8 +1,8 @@
using Dalamud.Data;
-using Dalamud.Game.Text.Evaluator;
using Dalamud.Game.Text.SeStringHandling;
+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;
@@ -12,7 +12,6 @@ using Lumina.Excel;
using Lumina.Text.ReadOnly;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
namespace Dalamud.Game.Chat;
@@ -150,9 +149,14 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa
// the formatting logic is taken from RaptureLogModule_Update
+ using var utf8 = new Utf8String();
SetName(logModule, this.SourceEntity);
SetName(logModule, this.TargetEntity);
- return Service.Get().EvaluateFromLogMessage(this.LogMessageId, ptr->Parameters.Select(p => (SeStringParameter)p).ToArray());
+
+ using var rssb = new RentedSeStringBuilder();
+ logModule->RaptureTextModule->FormatString(rssb.Builder.Append(this.GameData.Value.Text).GetViewAsSpan(), &ptr->Parameters, &utf8);
+
+ return new ReadOnlySeString(utf8.AsSpan());
void SetName(RaptureLogModule* self, LogMessageEntity item)
{
diff --git a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
index a8fe3b3b9..036d1c921 100644
--- a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
+++ b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs
@@ -1,7 +1,5 @@
using System.Globalization;
-using FFXIVClientStructs.FFXIV.Component.Text;
-
using Lumina.Text.ReadOnly;
using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
@@ -77,12 +75,4 @@ public readonly struct SeStringParameter
public static implicit operator SeStringParameter(string value) => new(value);
public static implicit operator SeStringParameter(ReadOnlySpan value) => new(value);
-
- public static unsafe implicit operator SeStringParameter(TextParameter value) => value.Type switch
- {
- TextParameterType.Uninitialized => default,
- TextParameterType.Integer => new((uint)value.IntValue),
- TextParameterType.ReferencedUtf8String => new(new ReadOnlySeString(value.ReferencedUtf8StringValue->Utf8String.AsSpan())),
- TextParameterType.String => new(value.StringValue),
- };
}
From 65c604f8272b6083175cd42945b4983b2f5cce98 Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Wed, 24 Dec 2025 11:13:06 +0100
Subject: [PATCH 08/13] More ReadOnlySeString things
---
Dalamud/Game/Chat/LogMessage.cs | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs
index 5d59a84ee..93b928d48 100644
--- a/Dalamud/Game/Chat/LogMessage.cs
+++ b/Dalamud/Game/Chat/LogMessage.cs
@@ -1,5 +1,4 @@
using Dalamud.Data;
-using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String;
@@ -64,7 +63,7 @@ public interface ILogMessage : IEquatable
/// The index of the parameter to retrieve.
/// The value of the parameter.
/// if the parameter was retrieved successfully.
- bool TryGetStringParameter(int index, [NotNullWhen(true)] out SeString? value);
+ 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.
@@ -124,18 +123,18 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa
}
///
- public bool TryGetStringParameter(int index, [NotNullWhen(true)] out SeString? value)
+ public bool TryGetStringParameter(int index, out ReadOnlySeString value)
{
- value = null;
+ value = default;
if (!this.TryGetParameter(index, out var parameter)) return false;
if (parameter.Type == TextParameterType.String)
{
- value = SeString.Parse(parameter.StringValue.Value);
+ value = new(parameter.StringValue.AsSpan());
return true;
}
if (parameter.Type == TextParameterType.ReferencedUtf8String)
{
- value = SeString.Parse(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan());
+ value = new(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan());
return true;
}
From 31cbf4d8eb47782cd332f41e0af8b74d10ebfb8a Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Wed, 24 Dec 2025 12:17:57 +0100
Subject: [PATCH 09/13] Respect null-termination of entity names
---
Dalamud/Game/Chat/LogMessageEntity.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Game/Chat/LogMessageEntity.cs b/Dalamud/Game/Chat/LogMessageEntity.cs
index b465dc158..4294e4898 100644
--- a/Dalamud/Game/Chat/LogMessageEntity.cs
+++ b/Dalamud/Game/Chat/LogMessageEntity.cs
@@ -52,7 +52,7 @@ internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool
{
public Span NameSpan => source ? ptr->SourceName : ptr->TargetName;
- public ReadOnlySeString Name => new ReadOnlySeString(this.NameSpan);
+ public ReadOnlySeString Name => new ReadOnlySeString(this.NameSpan[..this.NameSpan.IndexOf((byte)0)]);
public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld;
From c559426d8b6da76952b04a4cefc18fa6494d2c91 Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Wed, 24 Dec 2025 12:23:24 +0100
Subject: [PATCH 10/13] Add Data Widget
---
.../Internal/Windows/Data/DataWindow.cs | 1 +
.../Data/Widgets/LogMessageMonitorWidget.cs | 156 ++++++++++++++++++
2 files changed, 157 insertions(+)
create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/LogMessageMonitorWidget.cs
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
index eb0589d59..154fc8c02 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
@@ -44,6 +44,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);
+}
From 9b55b020ca32b0482b1306aebbbd6701ccd5fd47 Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Sat, 3 Jan 2026 23:11:50 +0100
Subject: [PATCH 11/13] Switch selftest to using mounts instead of teleporting
---
.../SelfTest/Steps/ChatSelfTestStep.cs | 54 +++++++++----------
1 file changed, 26 insertions(+), 28 deletions(-)
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
index 16fd3b01e..73f0405ef 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
@@ -1,10 +1,13 @@
using Dalamud.Bindings.ImGui;
+using Dalamud.Data;
using Dalamud.Game.Chat;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.SelfTest;
+using Lumina.Excel.Sheets;
+
namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps;
///
@@ -15,10 +18,10 @@ internal class ChatSelfTestStep : ISelfTestStep
private int step = 0;
private bool subscribedChatMessage = false;
private bool subscribedLogMessage = false;
- private bool hasPassed = false;
- private bool hasTeleportGil = false;
- private bool hasTeleportTicket = false;
- private int teleportCount = 0;
+ private bool hasSeenEchoMessage = false;
+ private bool hasSeenMountMessage = false;
+ private string mountName = "";
+ private string mountUser = "";
///
public string Name => "Test Chat";
@@ -45,7 +48,7 @@ internal class ChatSelfTestStep : ISelfTestStep
chatGui.ChatMessage += this.ChatOnOnChatMessage;
}
- if (this.hasPassed)
+ if (this.hasSeenEchoMessage)
{
chatGui.ChatMessage -= this.ChatOnOnChatMessage;
this.subscribedChatMessage = false;
@@ -55,7 +58,7 @@ internal class ChatSelfTestStep : ISelfTestStep
break;
case 2:
- ImGui.Text("Teleport somewhere...");
+ ImGui.Text("Use any mount...");
if (!this.subscribedLogMessage)
{
@@ -63,18 +66,11 @@ internal class ChatSelfTestStep : ISelfTestStep
chatGui.LogMessage += this.ChatOnLogMessage;
}
- if (this.hasTeleportGil)
+ if (this.hasSeenMountMessage)
{
- ImGui.Text($"You spent {this.teleportCount} gil to teleport.");
- }
- if (this.hasTeleportTicket)
- {
- ImGui.Text($"You used a ticket to teleport and have {this.teleportCount} remaining.");
- }
-
- if (this.hasTeleportGil || this.hasTeleportTicket)
- {
- ImGui.Text("Is this correct?");
+ ImGui.Text($"{this.mountUser} mounted {this.mountName}.");
+
+ ImGui.Text("Is this correct? It is correct if this triggers on other players around you.");
if (ImGui.Button("Yes"))
{
@@ -117,23 +113,25 @@ 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 == 4590 && message.TryGetIntParameter(0, out var value))
+ if (message.LogMessageId == 646 && message.TryGetIntParameter(0, out var value))
{
- this.hasTeleportGil = true;
- this.hasTeleportTicket = false;
- this.teleportCount = value;
- }
- if (message.LogMessageId == 4591 && message.TryGetIntParameter(0, out var item) && item == 7569 && message.TryGetIntParameter(1, out var remaining))
- {
- this.hasTeleportGil = false;
- this.hasTeleportTicket = true;
- this.teleportCount = remaining;
+ this.hasSeenMountMessage = true;
+ this.mountUser = message.SourceEntity?.Name.ExtractText() ?? "";
+ try
+ {
+ this.mountName = Service.Get().GetExcelSheet().GetRow((uint)value).Singular.ExtractText();
+ }
+ catch
+ {
+ // ignore any errors with retrieving the mount name, they are probably not related to this test
+ this.mountName = $"Mount ID: {value} (failed to retrieve mount name)";
+ }
}
}
}
From 790669e60abdc32e49e59c65315950340140b1a3 Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Sun, 4 Jan 2026 08:12:06 +0100
Subject: [PATCH 12/13] Battle log exists, selftest with the use action message
---
.../SelfTest/Steps/ChatSelfTestStep.cs | 34 ++++++-------------
1 file changed, 11 insertions(+), 23 deletions(-)
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
index 73f0405ef..d4311176e 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
@@ -1,13 +1,10 @@
using Dalamud.Bindings.ImGui;
-using Dalamud.Data;
using Dalamud.Game.Chat;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.SelfTest;
-using Lumina.Excel.Sheets;
-
namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps;
///
@@ -19,9 +16,9 @@ internal class ChatSelfTestStep : ISelfTestStep
private bool subscribedChatMessage = false;
private bool subscribedLogMessage = false;
private bool hasSeenEchoMessage = false;
- private bool hasSeenMountMessage = false;
- private string mountName = "";
- private string mountUser = "";
+ private bool hasSeenActionMessage = false;
+ private string actionName = "";
+ private string actionUser = "";
///
public string Name => "Test Chat";
@@ -58,7 +55,7 @@ internal class ChatSelfTestStep : ISelfTestStep
break;
case 2:
- ImGui.Text("Use any mount...");
+ ImGui.Text("Use any action (for example Sprint) or be near a player using an action.");
if (!this.subscribedLogMessage)
{
@@ -66,11 +63,10 @@ internal class ChatSelfTestStep : ISelfTestStep
chatGui.LogMessage += this.ChatOnLogMessage;
}
- if (this.hasSeenMountMessage)
+ if (this.hasSeenActionMessage)
{
- ImGui.Text($"{this.mountUser} mounted {this.mountName}.");
-
- ImGui.Text("Is this correct? It is correct if this triggers on other players around you.");
+ ImGui.Text($"{this.actionUser} used {this.actionName}.");
+ ImGui.Text("Is this correct?");
if (ImGui.Button("Yes"))
{
@@ -119,19 +115,11 @@ internal class ChatSelfTestStep : ISelfTestStep
private void ChatOnLogMessage(ILogMessage message)
{
- if (message.LogMessageId == 646 && message.TryGetIntParameter(0, out var value))
+ if (message.LogMessageId == 533 && message.TryGetStringParameter(0, out var value))
{
- this.hasSeenMountMessage = true;
- this.mountUser = message.SourceEntity?.Name.ExtractText() ?? "";
- try
- {
- this.mountName = Service.Get().GetExcelSheet().GetRow((uint)value).Singular.ExtractText();
- }
- catch
- {
- // ignore any errors with retrieving the mount name, they are probably not related to this test
- this.mountName = $"Mount ID: {value} (failed to retrieve mount name)";
- }
+ this.hasSeenActionMessage = true;
+ this.actionUser = message.SourceEntity?.Name.ExtractText() ?? "";
+ this.actionName = value.ExtractText();
}
}
}
From 6e19aca481a9c49ea0a46b94bdc8671c230f3f82 Mon Sep 17 00:00:00 2001
From: RedworkDE <10944644+RedworkDE@users.noreply.github.com>
Date: Sun, 4 Jan 2026 16:00:10 +0100
Subject: [PATCH 13/13] Fix StyleCop warnings
---
Dalamud/Game/Chat/LogMessage.cs | 75 ++++++++++---------
Dalamud/Game/Chat/LogMessageEntity.cs | 48 ++++++++----
.../SelfTest/Steps/ChatSelfTestStep.cs | 4 +-
Dalamud/Plugin/Services/IChatGui.cs | 1 -
4 files changed, 72 insertions(+), 56 deletions(-)
diff --git a/Dalamud/Game/Chat/LogMessage.cs b/Dalamud/Game/Chat/LogMessage.cs
index 93b928d48..c772783a1 100644
--- a/Dalamud/Game/Chat/LogMessage.cs
+++ b/Dalamud/Game/Chat/LogMessage.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
using Dalamud.Data;
using Dalamud.Utility;
@@ -10,8 +12,6 @@ using FFXIVClientStructs.Interop;
using Lumina.Excel;
using Lumina.Text.ReadOnly;
-using System.Diagnostics.CodeAnalysis;
-
namespace Dalamud.Game.Chat;
///
@@ -88,30 +88,41 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa
///
public RowRef GameData => LuminaUtils.CreateRef(ptr->LogMessageId);
- public LogMessageEntity SourceEntity => new LogMessageEntity(ptr, true);
///
ILogMessageEntity? ILogMessage.SourceEntity => ptr->SourceKind == EntityRelationKind.None ? null : this.SourceEntity;
- public LogMessageEntity TargetEntity => new LogMessageEntity(ptr, false);
-
///
ILogMessageEntity? ILogMessage.TargetEntity => ptr->TargetKind == EntityRelationKind.None ? null : this.TargetEntity;
///
public int ParameterCount => ptr->Parameters.Count;
- public bool TryGetParameter(int index, out TextParameter value)
- {
- if (index < 0 || index >= ptr->Parameters.Count)
- {
- value = default;
- return false;
- }
+ private LogMessageEntity SourceEntity => new(ptr, true);
- value = ptr->Parameters[index];
- return 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)
{
@@ -132,6 +143,7 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa
value = new(parameter.StringValue.AsSpan());
return true;
}
+
if (parameter.Type == TextParameterType.ReferencedUtf8String)
{
value = new(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan());
@@ -157,7 +169,7 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa
return new ReadOnlySeString(utf8.AsSpan());
- void SetName(RaptureLogModule* self, LogMessageEntity item)
+ static void SetName(RaptureLogModule* self, LogMessageEntity item)
{
var name = item.NameSpan.GetPointer(0);
@@ -190,31 +202,20 @@ internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessa
}
}
+ private bool TryGetParameter(int index, out TextParameter value)
+ {
+ if (index < 0 || index >= ptr->Parameters.Count)
+ {
+ value = default;
+ return false;
+ }
- public static bool operator ==(LogMessage x, LogMessage y) => x.Equals(y);
+ value = ptr->Parameters[index];
+ return true;
+ }
- public static bool operator !=(LogMessage x, LogMessage y) => !(x == y);
-
- public bool Equals(LogMessage other)
+ private bool Equals(LogMessage other)
{
return this.LogMessageId == other.LogMessageId && this.SourceEntity == other.SourceEntity && this.TargetEntity == other.TargetEntity;
}
-
- ///
- 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);
- }
}
diff --git a/Dalamud/Game/Chat/LogMessageEntity.cs b/Dalamud/Game/Chat/LogMessageEntity.cs
index 4294e4898..91e905928 100644
--- a/Dalamud/Game/Chat/LogMessageEntity.cs
+++ b/Dalamud/Game/Chat/LogMessageEntity.cs
@@ -37,46 +37,57 @@ public interface ILogMessageEntity : IEquatable
uint ObjStrId { get; }
///
- /// Gets a boolean indicating if this entity is a player.
+ /// 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
+/// 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 Span NameSpan => source ? ptr->SourceName : ptr->TargetName;
-
- public ReadOnlySeString Name => new ReadOnlySeString(this.NameSpan[..this.NameSpan.IndexOf((byte)0)]);
+ ///
+ 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 byte Kind => source ? (byte)ptr->SourceKind : (byte)ptr->TargetKind;
-
- public byte Sex => source ? ptr->SourceSex : ptr->TargetSex;
-
+ ///
public bool IsPlayer => source ? ptr->SourceIsPlayer : ptr->TargetIsPlayer;
- public bool IsSourceEntity => source;
+ ///
+ /// 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(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;
- }
-
///
public bool Equals(ILogMessageEntity other)
{
@@ -94,4 +105,9 @@ internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool
{
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/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
index d4311176e..851957b4b 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ChatSelfTestStep.cs
@@ -17,8 +17,8 @@ internal class ChatSelfTestStep : ISelfTestStep
private bool subscribedLogMessage = false;
private bool hasSeenEchoMessage = false;
private bool hasSeenActionMessage = false;
- private string actionName = "";
- private string actionUser = "";
+ private string actionName = string.Empty;
+ private string actionUser = string.Empty;
///
public string Name => "Test Chat";
diff --git a/Dalamud/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs
index eec25cb5a..2bb7b6913 100644
--- a/Dalamud/Plugin/Services/IChatGui.cs
+++ b/Dalamud/Plugin/Services/IChatGui.cs
@@ -51,7 +51,6 @@ 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.
///