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/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/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.
///