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