Merge branch 'master' into ImRaii-Widgets

This commit is contained in:
Infi 2026-01-10 14:13:41 +01:00 committed by GitHub
commit 5bfbcbb8f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 727 additions and 27 deletions

View file

@ -27,7 +27,11 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
private static readonly ModuleLog Log = ModuleLog.Create<AddonLifecycle>();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
private bool isInvokingListeners = false;
[ServiceManager.ServiceConstructor]
private AddonLifecycle()
@ -58,20 +62,23 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener)
{
if (!this.EventListeners.ContainsKey(listener.EventType))
this.framework.RunOnTick(() =>
{
if (!this.EventListeners.TryAdd(listener.EventType, []))
return;
}
if (!this.EventListeners.ContainsKey(listener.EventType))
{
if (!this.EventListeners.TryAdd(listener.EventType, []))
return;
}
// Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
return;
}
// Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
return;
}
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
}, delayTicks: this.isInvokingListeners ? 1 : 0);
}
/// <summary>
@ -80,13 +87,16 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener)
{
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
this.framework.RunOnTick(() =>
{
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
{
addonListener.Remove(listener);
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
{
addonListener.Remove(listener);
}
}
}
}, delayTicks: this.isInvokingListeners ? 1 : 0);
}
/// <summary>
@ -97,6 +107,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
this.isInvokingListeners = true;
// Early return if we don't have any listeners of this type
if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
@ -131,6 +143,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
}
}
}
this.isInvokingListeners = false;
}
/// <summary>

View file

@ -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;
/// <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, out ReadOnlySeString 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>
ReadOnlySeString FormatLogMessageForDebugging();
}
/// <summary>
/// This struct represents log message in the queue to be added to the chat.
/// </summary>
/// <param name="ptr">A pointer to the log message.</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);
/// <inheritdoc/>
ILogMessageEntity? ILogMessage.SourceEntity => ptr->SourceKind == EntityRelationKind.None ? null : this.SourceEntity;
/// <inheritdoc/>
ILogMessageEntity? ILogMessage.TargetEntity => ptr->TargetKind == EntityRelationKind.None ? null : this.TargetEntity;
/// <inheritdoc/>
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);
/// <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);
}
/// <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, 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;
}
/// <inheritdoc/>
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, "<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);
}
}
}
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;
}
}

View file

@ -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;
/// <summary>
/// Interface representing an entity related to a log message.
/// </summary>
public interface ILogMessageEntity : IEquatable<ILogMessageEntity>
{
/// <summary>
/// Gets the name of this entity.
/// </summary>
ReadOnlySeString 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 value indicating whether this entity is a player.
/// </summary>
bool IsPlayer { get; }
}
/// <summary>
/// This struct represents an entity related to a log message.
/// </summary>
/// <param name="ptr">A pointer to the log message item.</param>
/// <param name="source">If <see langword="true"/> represents the source entity of the log message, otherwise represents the target entity.</param>
internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool source) : ILogMessageEntity
{
/// <inheritdoc/>
public ReadOnlySeString Name => new(this.NameSpan[..this.NameSpan.IndexOf((byte)0)]);
/// <inheritdoc/>
public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld;
/// <inheritdoc/>
public RowRef<World> HomeWorld => LuminaUtils.CreateRef<World>(this.HomeWorldId);
/// <inheritdoc/>
public uint ObjStrId => source ? ptr->SourceObjStrId : ptr->TargetObjStrId;
/// <inheritdoc/>
public bool IsPlayer => source ? ptr->SourceIsPlayer : ptr->TargetIsPlayer;
/// <summary>
/// Gets the Span containing the raw name of this entity.
/// </summary>
internal Span<byte> NameSpan => source ? ptr->SourceName : ptr->TargetName;
/// <summary>
/// Gets the kind of the entity.
/// </summary>
internal byte Kind => source ? (byte)ptr->SourceKind : (byte)ptr->TargetKind;
/// <summary>
/// Gets the Sex of this entity.
/// </summary>
internal byte Sex => source ? ptr->SourceSex : ptr->TargetSex;
/// <summary>
/// Gets a value indicating whether this entity is the source entity of a log message.
/// </summary>
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);
/// <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);
}
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;
}
}

View file

@ -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 = [];
private readonly List<nint> seenLogMessageObjects = [];
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((nint)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);
}

View file

@ -32,6 +32,7 @@ using Dalamud.Interface.Windowing;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
@ -502,6 +503,34 @@ internal partial class InterfaceManager : IInternalDisposableService
ImGuiHelpers.ClearStacksOnContext();
}
/// <summary>
/// Applies immersive dark mode to the game window based on the current system theme setting.
/// </summary>
internal void SetImmersiveModeFromSystemTheme()
{
bool useDark = this.IsSystemInDarkMode();
this.SetImmersiveMode(useDark);
}
/// <summary>
/// Checks whether the system use dark mode.
/// </summary>
/// <returns>Returns true if dark mode is preferred.</returns>
internal bool IsSystemInDarkMode()
{
try
{
using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(
@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
var value = key?.GetValue("AppsUseLightTheme") as int?;
return value != 1;
}
catch
{
return false;
}
}
/// <summary>
/// Toggle Windows 11 immersive mode on the game window.
/// </summary>
@ -745,6 +774,18 @@ internal partial class InterfaceManager : IInternalDisposableService
private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args)
{
if (args.Message == WM.WM_SETTINGCHANGE)
{
if (this.dalamudConfiguration.WindowIsImmersive)
{
if (MemoryHelper.EqualsZeroTerminatedWideString("ImmersiveColorSet", args.LParam) ||
MemoryHelper.EqualsZeroTerminatedWideString("VisualStyleChanged", args.LParam))
{
this.SetImmersiveModeFromSystemTheme();
}
}
}
var r = this.backend?.ProcessWndProcW(args.Hwnd, args.Message, args.WParam, args.LParam);
if (r is not null)
args.SuppressWithValue(r.Value);
@ -859,7 +900,7 @@ internal partial class InterfaceManager : IInternalDisposableService
{
// Requires that game window to be there, which will be the case once game swap chain is initialized.
if (Service<DalamudConfiguration>.Get().WindowIsImmersive)
this.SetImmersiveMode(true);
this.SetImmersiveModeFromSystemTheme();
}
catch (Exception ex)
{

View file

@ -45,6 +45,7 @@ internal class DataWindow : Window, IDisposable
new ImGuiWidget(),
new InventoryWidget(),
new KeyStateWidget(),
new LogMessageMonitorWidget(),
new MarketBoardWidget(),
new NetworkMonitorWidget(),
new NounProcessorWidget(),

View file

@ -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;
/// <summary>
/// Widget to display the LogMessages.
/// </summary>
internal class LogMessageMonitorWidget : IDataWindowWidget
{
private readonly ConcurrentQueue<LogMessageData> messages = new();
private bool trackMessages;
private int trackedMessages;
private Regex? filterRegex;
private string filterString = string.Empty;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = ["logmessage"];
/// <inheritdoc/>
public string DisplayName { get; init; } = "LogMessage Monitor";
/// <inheritdoc/>
public bool Ready { get; set; }
/// <inheritdoc/>
public void Load()
{
this.trackMessages = false;
this.trackedMessages = 20;
this.filterRegex = null;
this.filterString = string.Empty;
this.messages.Clear();
this.Ready = true;
}
/// <inheritdoc/>
public void Draw()
{
var network = Service<ChatGui>.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<byte>();
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<byte> Parameters, ReadOnlySeString Formatted);
}

View file

@ -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;
/// <inheritdoc/>
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<ChatGui>.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() ?? "<incorrect>";
this.actionName = value.ExtractText();
}
}
}

View file

@ -66,7 +66,14 @@ internal sealed class SettingsTabLook : SettingsTab
{
try
{
Service<InterfaceManager>.GetNullable()?.SetImmersiveMode(b);
if (b)
{
Service<InterfaceManager>.GetNullable()?.SetImmersiveModeFromSystemTheme();
}
else
{
Service<InterfaceManager>.GetNullable()?.SetImmersiveMode(false);
}
}
catch (Exception ex)
{

View file

@ -268,6 +268,31 @@ public static unsafe class MemoryHelper
}
}
/// <summary>
/// Compares a UTF-16 character span with a null-terminated UTF-16 string at <paramref name="memoryAddress"/>.
/// </summary>
/// <param name="charSpan">UTF-16 character span (e.g., from a string literal).</param>
/// <param name="memoryAddress">Address of null-terminated UTF-16 (wide) string, as used by Windows APIs.</param>
/// <returns><see langword="true"/> if equal; otherwise, <see langword="false"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool EqualsZeroTerminatedWideString(
scoped ReadOnlySpan<char> charSpan,
nint memoryAddress)
{
if (memoryAddress == 0)
return charSpan.Length == 0;
char* p = (char*)memoryAddress;
foreach (char c in charSpan)
{
if (*p++ != c)
return false;
}
return *p == '\0';
}
/// <summary>
/// Read a UTF-8 encoded string from a specified memory address.
/// </summary>

View file

@ -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
/// <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 +77,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>

@ -1 +1 @@
Subproject commit ae1917bf103926bfd157c7d911efac58c0e28666
Subproject commit d83e0c13d3c802d4a483f373edcd129bc4802073