mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-07 08:24:37 +01:00
Add event for LogMessages being added to the chat
This commit is contained in:
parent
3be14d4135
commit
282fa87571
4 changed files with 387 additions and 0 deletions
220
Dalamud/Game/Chat/LogMessage.cs
Normal file
220
Dalamud/Game/Chat/LogMessage.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing a log message.
|
||||
/// </summary>
|
||||
public interface ILogMessage : IEquatable<ILogMessage>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the address of the log message in memory.
|
||||
/// </summary>
|
||||
nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of this log message.
|
||||
/// </summary>
|
||||
uint LogMessageId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the GameData associated with this log message.
|
||||
/// </summary>
|
||||
RowRef<Lumina.Excel.Sheets.LogMessage> GameData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity that is the source of this log message, if any.
|
||||
/// </summary>
|
||||
ILogMessageEntity? SourceEntity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entity that is the target of this log message, if any.
|
||||
/// </summary>
|
||||
ILogMessageEntity? TargetEntity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of parameters.
|
||||
/// </summary>
|
||||
int ParameterCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the value of a parameter for the log message if it is an int.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the parameter to retrieve.</param>
|
||||
/// <param name="value">The value of the parameter.</param>
|
||||
/// <returns><see langword="true"/> if the parameter was retrieved successfully.</returns>
|
||||
bool TryGetIntParameter(int index, out int value);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the value of a parameter for the log message if it is a string.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the parameter to retrieve.</param>
|
||||
/// <param name="value">The value of the parameter.</param>
|
||||
/// <returns><see langword="true"/> if the parameter was retrieved successfully.</returns>
|
||||
bool TryGetStringParameter(int index, [NotNullWhen(true)] out SeString? value);
|
||||
|
||||
/// <summary>
|
||||
/// Formats this log message into an approximation of the string that will eventually be shown in the log.
|
||||
/// </summary>
|
||||
/// <remarks>This can cause side effects such as playing sound effects and thus should only be used for debugging.</remarks>
|
||||
/// <returns>The formatted string.</returns>
|
||||
SeString FormatLogMessageForDebugging();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This struct represents a status effect an actor is afflicted by.
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the Status.</param>
|
||||
internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public nint Address => (nint)ptr;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public uint LogMessageId => ptr->LogMessageId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RowRef<Lumina.Excel.Sheets.LogMessage> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.LogMessage>(ptr->LogMessageId);
|
||||
|
||||
public LogMessageEntity SourceEntity => new LogMessageEntity(ptr, true);
|
||||
/// <inheritdoc/>
|
||||
ILogMessageEntity? ILogMessage.SourceEntity => ptr->SourceKind == EntityRelationKind.None ? null : this.SourceEntity;
|
||||
|
||||
public LogMessageEntity TargetEntity => new LogMessageEntity(ptr, false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
ILogMessageEntity? ILogMessage.TargetEntity => ptr->TargetKind == EntityRelationKind.None ? null : this.TargetEntity;
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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, "<icon(88)>\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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(ILogMessage? other)
|
||||
{
|
||||
return other is LogMessage logMessage && this.Equals(logMessage);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is LogMessage logMessage && this.Equals(logMessage);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.LogMessageId, this.SourceEntity, this.TargetEntity);
|
||||
}
|
||||
}
|
||||
96
Dalamud/Game/Chat/LogMessageEntity.cs
Normal file
96
Dalamud/Game/Chat/LogMessageEntity.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing an entity related to a log message.
|
||||
/// </summary>
|
||||
public interface ILogMessageEntity : IEquatable<ILogMessageEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of this entity.
|
||||
/// </summary>
|
||||
SeString Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the homeworld of this entity, if it is a player.
|
||||
/// </summary>
|
||||
ushort HomeWorldId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the homeworld of this entity, if it is a player.
|
||||
/// </summary>
|
||||
RowRef<World> HomeWorld { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ObjStr ID of this entity, if not a player. See <seealso cref="ISeStringEvaluator.EvaluateObjStr"/>.
|
||||
/// </summary>
|
||||
uint ObjStrId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating if this entity is a player.
|
||||
/// </summary>
|
||||
bool IsPlayer { get; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This struct represents a status effect an actor is afflicted by.
|
||||
/// </summary>
|
||||
/// <param name="ptr">A pointer to the Status.</param>
|
||||
internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool source) : ILogMessageEntity
|
||||
{
|
||||
public Span<byte> NameSpan => source ? ptr->SourceName : ptr->TargetName;
|
||||
|
||||
public SeString Name => SeString.Parse(this.NameSpan);
|
||||
|
||||
public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld;
|
||||
|
||||
public RowRef<World> HomeWorld => LuminaUtils.CreateRef<World>(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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(ILogMessageEntity other)
|
||||
{
|
||||
return other is LogMessageEntity entity && this.Equals(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is LogMessageEntity entity && this.Equals(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Name, this.HomeWorldId, this.ObjStrId, this.Sex, this.IsPlayer);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<XivChatEntry> chatQueue = new();
|
||||
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
|
||||
private readonly List<IntPtr> seenLogMessageObjects = new();
|
||||
|
||||
private readonly Hook<PrintMessageDelegate> printMessageHook;
|
||||
private readonly Hook<InventoryItem.Delegates.Copy> inventoryItemCopyHook;
|
||||
private readonly Hook<LogViewer.Delegates.HandleLinkClick> handleLinkClickHook;
|
||||
private readonly Hook<RaptureLogModule.Delegates.Update> handleLogModuleUpdate;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
||||
|
|
@ -58,10 +61,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
|
|||
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
|
||||
this.inventoryItemCopyHook = Hook<InventoryItem.Delegates.Copy>.FromAddress((nint)InventoryItem.StaticVirtualTablePointer->Copy, this.InventoryItemCopyDetour);
|
||||
this.handleLinkClickHook = Hook<LogViewer.Delegates.HandleLinkClick>.FromAddress(LogViewer.Addresses.HandleLinkClick.Value, this.HandleLinkClickDetour);
|
||||
this.handleLogModuleUpdate = Hook<RaptureLogModule.Delegates.Update>.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
|
|||
/// <inheritdoc/>
|
||||
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IChatGui.OnLogMessageDelegate? LogMessage;
|
||||
|
||||
/// <inheritdoc/>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -535,6 +585,9 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
|
|||
/// <inheritdoc/>
|
||||
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IChatGui.OnLogMessageDelegate? LogMessage;
|
||||
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// <param name="message">The message sent.</param>
|
||||
public delegate void OnMessageUnhandledDelegate(XivChatType type, int timestamp, SeString sender, SeString message);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A delegate type used with the <see cref="IChatGui.LogMessage"/> event.
|
||||
/// </summary>
|
||||
/// <param name="message">The message sent.</param>
|
||||
public delegate void OnLogMessageDelegate(ILogMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be fired when a chat message is sent to chat by the game.
|
||||
/// </summary>
|
||||
|
|
@ -70,6 +78,11 @@ public interface IChatGui : IDalamudService
|
|||
/// </summary>
|
||||
public event OnMessageUnhandledDelegate ChatMessageUnhandled;
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be fired when a log message, that is a chat message based on entries in the LogMessage sheet, is sent.
|
||||
/// </summary>
|
||||
public event OnLogMessageDelegate LogMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the last linked item.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue