From afa7b0c1f3d4330c1ec87bee17876978d46f30ab Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:30:58 +0100 Subject: [PATCH 01/10] Improve parameter verification logic in HookVerifier (#2596) * Improve parameter verification logic in HookVerifier Refactor HookVerifier to enhance parameter type checking and add utility methods for size calculations. * Reverse bool check * Fix type size check on return type * Fix non static member in static class * Fix compiler errors * Fix SizeOf calls * Fix IsStruct call * Cleanup some warnings --- .../Internal/Verification/HookVerifier.cs | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs index ad68ae38e..ebe6851ce 100644 --- a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -1,8 +1,13 @@ using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Logging.Internal; +using InteropGenerator.Runtime; + namespace Dalamud.Hooking.Internal.Verification; /// @@ -19,11 +24,13 @@ internal static class HookVerifier new( "ActorControlSelf", "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", - typeof(ActorControlSelfDelegate), + typeof(ActorControlSelfDelegate), // TODO: change this to CS delegate "Signature changed in Patch 7.4") // 7.4 (new parameters) ]; - private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9); + private static readonly string ClientStructsInteropNamespacePrefix = string.Join(".", nameof(FFXIVClientStructs), nameof(FFXIVClientStructs.Interop)); + + private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9); // TODO: change this to CS delegate /// /// Initializes a new instance of the class. @@ -71,7 +78,7 @@ internal static class HookVerifier var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!; // Compare Return Type - var mismatch = passedInvoke.ReturnType != enforcedInvoke.ReturnType; + var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType); // Compare Parameter Count var passedParams = passedInvoke.GetParameters(); @@ -86,7 +93,7 @@ internal static class HookVerifier // Compare Parameter Types for (var i = 0; i < passedParams.Length; i++) { - if (passedParams[i].ParameterType != enforcedParams[i].ParameterType) + if (!CheckParam(passedParams[i].ParameterType, enforcedParams[i].ParameterType)) { mismatch = true; break; @@ -100,6 +107,45 @@ internal static class HookVerifier } } + private static bool CheckParam(Type paramLeft, Type paramRight) + { + var sameType = paramLeft == paramRight; + return sameType || SizeOf(paramLeft) == SizeOf(paramRight); + } + + private static int SizeOf(Type type) + { + return type switch { + _ when type == typeof(sbyte) || type == typeof(byte) || type == typeof(bool) => 1, + _ when type == typeof(char) || type == typeof(short) || type == typeof(ushort) || type == typeof(Half) => 2, + _ when type == typeof(int) || type == typeof(uint) || type == typeof(float) => 4, + _ when type == typeof(long) || type == typeof(ulong) || type == typeof(double) || type.IsPointer || type.IsFunctionPointer || type.IsUnmanagedFunctionPointer || (type.Name == "Pointer`1" && type.Namespace.AsSpan().SequenceEqual(ClientStructsInteropNamespacePrefix)) || type == typeof(CStringPointer) => 8, + _ when type.Name.StartsWith("FixedSizeArray") => SizeOf(type.GetGenericArguments()[0]) * int.Parse(type.Name[14..type.Name.IndexOf('`')]), + _ when type.GetCustomAttribute() is { Length: var length } => SizeOf(type.GetGenericArguments()[0]) * length, + _ when IsStruct(type) && !type.IsGenericType && (type.StructLayoutAttribute?.Value ?? LayoutKind.Sequential) != LayoutKind.Sequential => type.StructLayoutAttribute?.Size ?? (int?)typeof(Unsafe).GetMethod("SizeOf")?.MakeGenericMethod(type).Invoke(null, null) ?? 0, + _ when type.IsEnum => SizeOf(Enum.GetUnderlyingType(type)), + _ when type.IsGenericType => Marshal.SizeOf(Activator.CreateInstance(type)!), + _ => GetSizeOf(type), + }; + } + + private static int GetSizeOf(Type type) + { + try + { + return Marshal.SizeOf(Activator.CreateInstance(type)!); + } + catch + { + return 0; + } + } + + private static bool IsStruct(Type type) + { + return type != typeof(decimal) && type is { IsValueType: true, IsPrimitive: false, IsEnum: false }; + } + private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message) { public nint Address { get; set; } From 3abf7bb00bc834be650f784a322870510245fefa Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 27 Jan 2026 17:35:55 +0100 Subject: [PATCH 02/10] Rework NetworkMonitorWidget, remove GameNetwork (#2593) * Rework NetworkMonitorWidget, remove GameNetwork * Rework packet filtering --- Dalamud/Game/Network/GameNetwork.cs | 147 -------- .../Network/GameNetworkAddressResolver.cs | 20 - .../Game/Network/Internal/NetworkHandlers.cs | 5 +- .../Game/Network/NetworkMessageDirection.cs | 17 - .../Data/Widgets/NetworkMonitorWidget.cs | 341 +++++++++++------- Dalamud/Plugin/Services/IGameNetwork.cs | 27 -- 6 files changed, 212 insertions(+), 345 deletions(-) delete mode 100644 Dalamud/Game/Network/GameNetwork.cs delete mode 100644 Dalamud/Game/Network/GameNetworkAddressResolver.cs delete mode 100644 Dalamud/Game/Network/NetworkMessageDirection.cs delete mode 100644 Dalamud/Plugin/Services/IGameNetwork.cs diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs deleted file mode 100644 index b8c91b235..000000000 --- a/Dalamud/Game/Network/GameNetwork.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Runtime.InteropServices; - -using Dalamud.Configuration.Internal; -using Dalamud.Hooking; -using Dalamud.Utility; - -using FFXIVClientStructs.FFXIV.Client.Network; - -using Serilog; - -namespace Dalamud.Game.Network; - -/// -/// This class handles interacting with game network events. -/// -[ServiceManager.EarlyLoadedService] -internal sealed unsafe class GameNetwork : IInternalDisposableService -{ - private readonly GameNetworkAddressResolver address; - private readonly Hook processZonePacketDownHook; - private readonly Hook processZonePacketUpHook; - - private readonly HitchDetector hitchDetectorUp; - private readonly HitchDetector hitchDetectorDown; - - [ServiceManager.ServiceDependency] - private readonly DalamudConfiguration configuration = Service.Get(); - - [ServiceManager.ServiceConstructor] - private unsafe GameNetwork(TargetSigScanner sigScanner) - { - this.hitchDetectorUp = new HitchDetector("GameNetworkUp", this.configuration.GameNetworkUpHitch); - this.hitchDetectorDown = new HitchDetector("GameNetworkDown", this.configuration.GameNetworkDownHitch); - - this.address = new GameNetworkAddressResolver(); - this.address.Setup(sigScanner); - - var onReceivePacketAddress = (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket; - - Log.Verbose("===== G A M E N E T W O R K ====="); - Log.Verbose($"OnReceivePacket address {Util.DescribeAddress(onReceivePacketAddress)}"); - Log.Verbose($"ProcessZonePacketUp address {Util.DescribeAddress(this.address.ProcessZonePacketUp)}"); - - this.processZonePacketDownHook = Hook.FromAddress(onReceivePacketAddress, this.ProcessZonePacketDownDetour); - this.processZonePacketUpHook = Hook.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour); - - this.processZonePacketDownHook.Enable(); - this.processZonePacketUpHook.Enable(); - } - - /// - /// The delegate type of a network message event. - /// - /// The pointer to the raw data. - /// The operation ID code. - /// The source actor ID. - /// The taret actor ID. - /// The direction of the packed. - public delegate void OnNetworkMessageDelegate(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction); - - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate byte ProcessZonePacketUpDelegate(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4); - - /// - /// Event that is called when a network message is sent/received. - /// - public event OnNetworkMessageDelegate? NetworkMessage; - - /// - void IInternalDisposableService.DisposeService() - { - this.processZonePacketDownHook.Dispose(); - this.processZonePacketUpHook.Dispose(); - } - - private void ProcessZonePacketDownDetour(PacketDispatcher* dispatcher, uint targetId, IntPtr dataPtr) - { - this.hitchDetectorDown.Start(); - - // Go back 0x10 to get back to the start of the packet header - dataPtr -= 0x10; - - foreach (var d in Delegate.EnumerateInvocationList(this.NetworkMessage)) - { - try - { - d.Invoke( - dataPtr + 0x20, - (ushort)Marshal.ReadInt16(dataPtr, 0x12), - 0, - targetId, - NetworkMessageDirection.ZoneDown); - } - catch (Exception ex) - { - string header; - try - { - var data = new byte[32]; - Marshal.Copy(dataPtr, data, 0, 32); - header = BitConverter.ToString(data); - } - catch (Exception) - { - header = "failed"; - } - - Log.Error(ex, "Exception on ProcessZonePacketDown hook. Header: " + header); - } - } - - this.processZonePacketDownHook.Original(dispatcher, targetId, dataPtr + 0x10); - this.hitchDetectorDown.Stop(); - } - - private byte ProcessZonePacketUpDetour(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4) - { - this.hitchDetectorUp.Start(); - - try - { - // Call events - // TODO: Implement actor IDs - this.NetworkMessage?.Invoke(dataPtr + 0x20, (ushort)Marshal.ReadInt16(dataPtr), 0x0, 0x0, NetworkMessageDirection.ZoneUp); - } - catch (Exception ex) - { - string header; - try - { - var data = new byte[32]; - Marshal.Copy(dataPtr, data, 0, 32); - header = BitConverter.ToString(data); - } - catch (Exception) - { - header = "failed"; - } - - Log.Error(ex, "Exception on ProcessZonePacketUp hook. Header: " + header); - } - - this.hitchDetectorUp.Stop(); - - return this.processZonePacketUpHook.Original(a1, dataPtr, a3, a4); - } -} diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs deleted file mode 100644 index 48abc2d97..000000000 --- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Dalamud.Plugin.Services; - -namespace Dalamud.Game.Network; - -/// -/// The address resolver for the class. -/// -internal sealed class GameNetworkAddressResolver : BaseAddressResolver -{ - /// - /// Gets the address of the ProcessZonePacketUp method. - /// - public IntPtr ProcessZonePacketUp { get; private set; } - - /// - protected override void Setup64Bit(ISigScanner sig) - { - this.ProcessZonePacketUp = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 4C 89 64 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 70"); // unnamed in cs - } -} diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 5ca7da54a..d3a53b4f2 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -55,10 +55,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService private bool disposing; [ServiceManager.ServiceConstructor] - private NetworkHandlers( - GameNetwork gameNetwork, - TargetSigScanner sigScanner, - HappyHttpClient happyHttpClient) + private NetworkHandlers(TargetSigScanner sigScanner, HappyHttpClient happyHttpClient) { this.uploader = new UniversalisMarketBoardUploader(happyHttpClient); diff --git a/Dalamud/Game/Network/NetworkMessageDirection.cs b/Dalamud/Game/Network/NetworkMessageDirection.cs deleted file mode 100644 index 87cce5173..000000000 --- a/Dalamud/Game/Network/NetworkMessageDirection.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Dalamud.Game.Network; - -/// -/// This represents the direction of a network message. -/// -public enum NetworkMessageDirection -{ - /// - /// A zone down message. - /// - ZoneDown, - - /// - /// A zone up message. - /// - ZoneUp, -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index ae173578a..922b72717 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -1,44 +1,51 @@ using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; +using System.Threading; using Dalamud.Bindings.ImGui; -using Dalamud.Game.Network; -using Dalamud.Interface.Utility; +using Dalamud.Game; +using Dalamud.Hooking; +using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; -using Dalamud.Memory; -using ImGuiTable = Dalamud.Interface.Utility.ImGuiTable; +using FFXIVClientStructs.FFXIV.Application.Network; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.Network; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget to display the current packets. /// -internal class NetworkMonitorWidget : IDataWindowWidget +internal unsafe class NetworkMonitorWidget : IDataWindowWidget { private readonly ConcurrentQueue packets = new(); + private Hook? hookDown; + private Hook? hookUp; + private bool trackNetwork; - private int trackedPackets; - private Regex? trackedOpCodes; + private int trackedPackets = 20; + private ulong nextPacketIndex; private string filterString = string.Empty; - private Regex? untrackedOpCodes; - private string negativeFilterString = string.Empty; + private bool filterRecording = true; + private bool autoScroll = true; + private bool autoScrollPending; /// Finalizes an instance of the class. ~NetworkMonitorWidget() { - if (this.trackNetwork) - { - this.trackNetwork = false; - var network = Service.GetNullable(); - if (network != null) - { - network.NetworkMessage -= this.OnNetworkMessage; - } - } + this.hookDown?.Dispose(); + this.hookUp?.Dispose(); + } + + private delegate byte ZoneClientSendPacketDelegate(ZoneClient* thisPtr, nint packet, uint a3, uint a4, byte a5); + + private enum NetworkMessageDirection + { + ZoneDown, + ZoneUp, } /// @@ -53,31 +60,36 @@ internal class NetworkMonitorWidget : IDataWindowWidget /// public void Load() { - this.trackNetwork = false; - this.trackedPackets = 20; - this.trackedOpCodes = null; - this.filterString = string.Empty; - this.packets.Clear(); + this.hookDown = Hook.FromAddress( + (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket, + this.OnReceivePacketDetour); + + // TODO: switch to ZoneClient.SendPacket from CS + if (Service.Get().TryScanText("E8 ?? ?? ?? ?? 4C 8B 44 24 ?? E9", out var address)) + this.hookUp = Hook.FromAddress(address, this.SendPacketDetour); + this.Ready = true; } /// public void Draw() { - var network = Service.Get(); if (ImGui.Checkbox("Track Network Packets"u8, ref this.trackNetwork)) { if (this.trackNetwork) { - network.NetworkMessage += this.OnNetworkMessage; + this.nextPacketIndex = 0; + this.hookDown?.Enable(); + this.hookUp?.Enable(); } else { - network.NetworkMessage -= this.OnNetworkMessage; + this.hookDown?.Disable(); + this.hookUp?.Disable(); } } - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2); + ImGui.SetNextItemWidth(-1); if (ImGui.DragInt("Stored Number of Packets"u8, ref this.trackedPackets, 0.1f, 1, 512)) { this.trackedPackets = Math.Clamp(this.trackedPackets, 1, 512); @@ -88,131 +100,200 @@ internal class NetworkMonitorWidget : IDataWindowWidget this.packets.Clear(); } - this.DrawFilterInput(); - this.DrawNegativeFilterInput(); + ImGui.SameLine(); + ImGui.Checkbox("Auto-Scroll"u8, ref this.autoScroll); - ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "OpCode", "Hex", "Target", "Source", "Data"); - } + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight()) * 2); + ImGui.InputTextWithHint("##Filter"u8, "Filter OpCodes..."u8, ref this.filterString, 1024, ImGuiInputTextFlags.AutoSelectAll); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.Checkbox("##FilterRecording"u8, ref this.filterRecording); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Apply filter to incoming packets.\nUncheck to record all packets and filter the table instead."u8); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGuiComponents.HelpMarker("Enter OpCodes in a comma-separated list.\nRanges are supported. Exclude OpCodes with exclamation mark.\nExample: -400,!50-100,650,700-980,!941"); - private void DrawNetworkPacket(NetworkPacketData data) - { - ImGui.TableNextColumn(); - ImGui.Text(data.Direction.ToString()); + using var table = ImRaii.Table("NetworkMonitorTableV2"u8, 6, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.NoSavedSettings); + if (!table) return; - ImGui.TableNextColumn(); - ImGui.Text(data.OpCode.ToString()); + ImGui.TableSetupColumn("Index"u8, ImGuiTableColumnFlags.WidthFixed, 50); + ImGui.TableSetupColumn("Time"u8, ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Direction"u8, ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("OpCode"u8, ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("OpCode (Hex)"u8, ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Target EntityId"u8, ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); - ImGui.TableNextColumn(); - ImGui.Text($"0x{data.OpCode:X4}"); + var autoScrollDisabled = false; - ImGui.TableNextColumn(); - ImGui.Text(data.TargetActorId > 0 ? $"0x{data.TargetActorId:X}" : string.Empty); - - ImGui.TableNextColumn(); - ImGui.Text(data.SourceActorId > 0 ? $"0x{data.SourceActorId:X}" : string.Empty); - - ImGui.TableNextColumn(); - if (data.Data.Count > 0) + foreach (var packet in this.packets) { - ImGui.Text(string.Join(" ", data.Data.Select(b => b.ToString("X2")))); + if (!this.filterRecording && !this.IsFiltered(packet.OpCode)) + continue; + + ImGui.TableNextColumn(); + ImGui.Text(packet.Index.ToString()); + + ImGui.TableNextColumn(); + ImGui.Text(packet.Time.ToLongTimeString()); + + ImGui.TableNextColumn(); + ImGui.Text(packet.Direction.ToString()); + + ImGui.TableNextColumn(); + using (ImRaii.PushId(packet.Index.ToString())) + { + if (ImGui.SmallButton("X")) + { + if (!string.IsNullOrEmpty(this.filterString)) + this.filterString += ","; + + this.filterString += $"!{packet.OpCode}"; + } + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Filter OpCode"u8); + + autoScrollDisabled |= ImGui.IsItemHovered(); + + ImGui.SameLine(); + WidgetUtil.DrawCopyableText(packet.OpCode.ToString()); + autoScrollDisabled |= ImGui.IsItemHovered(); + + ImGui.TableNextColumn(); + WidgetUtil.DrawCopyableText($"0x{packet.OpCode:X3}"); + autoScrollDisabled |= ImGui.IsItemHovered(); + + ImGui.TableNextColumn(); + if (packet.TargetEntityId > 0) + { + WidgetUtil.DrawCopyableText($"{packet.TargetEntityId:X}"); + + var name = !string.IsNullOrEmpty(packet.TargetName) + ? packet.TargetName + : GetTargetName(packet.TargetEntityId); + + if (!string.IsNullOrEmpty(name)) + { + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.Text($"({name})"); + } + } } - else + + if (this.autoScroll && this.autoScrollPending && !autoScrollDisabled) { - ImGui.Dummy(ImGui.GetContentRegionAvail() with { Y = 0 }); + ImGui.SetScrollHereY(); + this.autoScrollPending = false; } } - private void DrawFilterInput() + private static string GetTargetName(uint targetId) { - var invalidRegEx = this.filterString.Length > 0 && this.trackedOpCodes == 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 OpCodes..."u8, ref this.filterString, 1024)) - { + if (targetId == PlayerState.Instance()->EntityId) + return "Local Player"; + + var cachedName = NameCache.Instance()->GetNameByEntityId(targetId); + if (cachedName.HasValue) + return cachedName.ToString(); + + var obj = GameObjectManager.Instance()->Objects.GetObjectByEntityId(targetId); + if (obj != null) + return obj->NameString; + + return string.Empty; + } + + private void OnReceivePacketDetour(PacketDispatcher* thisPtr, uint targetId, nint packet) + { + var opCode = *(ushort*)(packet + 2); + var targetName = GetTargetName(targetId); + this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneDown, targetId, targetName)); + this.hookDown.OriginalDisposeSafe(thisPtr, targetId, packet); + } + + private byte SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, byte a5) + { + var opCode = *(ushort*)packet; + this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty)); + return this.hookUp.OriginalDisposeSafe(thisPtr, packet, a3, a4, a5); + } + + private void RecordPacket(NetworkPacketData packet) + { + if (this.filterRecording && !this.IsFiltered(packet.OpCode)) return; + + this.packets.Enqueue(packet); + + while (this.packets.Count > this.trackedPackets) + { + this.packets.TryDequeue(out _); } - if (this.filterString.Length == 0) - { - this.trackedOpCodes = null; - } - else - { - try - { - this.trackedOpCodes = new Regex(this.filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture); - } - catch - { - this.trackedOpCodes = null; - } - } + this.autoScrollPending = true; } - private void DrawNegativeFilterInput() + private bool IsFiltered(ushort opcode) { - var invalidRegEx = this.negativeFilterString.Length > 0 && this.untrackedOpCodes == 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("##NegativeFilter"u8, "Regex Filter Against OpCodes..."u8, ref this.negativeFilterString, 1024)) - { - return; - } + var filterString = this.filterString.Replace(" ", string.Empty); - if (this.negativeFilterString.Length == 0) + if (filterString.Length == 0) + return true; + + try { - this.untrackedOpCodes = null; + var offset = 0; + var included = false; + var hasInclude = false; + + while (filterString.Length - offset > 0) + { + var remaining = filterString[offset..]; + + // find the end of the current entry + var entryEnd = remaining.IndexOf(','); + if (entryEnd == -1) + entryEnd = remaining.Length; + + var entry = filterString[offset..(offset + entryEnd)]; + var dash = entry.IndexOf('-'); + var isExcluded = entry.StartsWith('!'); + var startOffset = isExcluded ? 1 : 0; + + var entryMatch = dash == -1 + ? ushort.Parse(entry[startOffset..]) == opcode + : ((dash - startOffset == 0 || opcode >= ushort.Parse(entry[startOffset..dash])) + && (entry[(dash + 1)..].Length == 0 || opcode <= ushort.Parse(entry[(dash + 1)..]))); + + if (isExcluded) + { + if (entryMatch) + return false; + } + else + { + hasInclude = true; + included |= entryMatch; + } + + if (entryEnd == filterString.Length) + break; + + offset += entryEnd + 1; + } + + return !hasInclude || included; } - else + catch (Exception ex) { - try - { - this.untrackedOpCodes = new Regex(this.negativeFilterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture); - } - catch - { - this.untrackedOpCodes = null; - } + Serilog.Log.Error(ex, "Invalid filter string"); + return false; } } - private void OnNetworkMessage(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) - { - if ((this.trackedOpCodes == null || this.trackedOpCodes.IsMatch(this.OpCodeToString(opCode))) - && (this.untrackedOpCodes == null || !this.untrackedOpCodes.IsMatch(this.OpCodeToString(opCode)))) - { - this.packets.Enqueue(new NetworkPacketData(this, opCode, direction, sourceActorId, targetActorId, dataPtr)); - while (this.packets.Count > this.trackedPackets) - { - this.packets.TryDequeue(out _); - } - } - } - - private int GetSizeFromOpCode(ushort opCode) - => 0; - - /// Add known packet-name -> packet struct size associations here to copy the byte data for such packets. > - private int GetSizeFromName(string name) - => name switch - { - _ => 0, - }; - - /// The filter should find opCodes by number (decimal and hex) and name, if existing. - private string OpCodeToString(ushort opCode) - => $"{opCode}\0{opCode:X}"; - #pragma warning disable SA1313 - private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId) + private readonly record struct NetworkPacketData(ulong Index, DateTime Time, ushort OpCode, NetworkMessageDirection Direction, uint TargetEntityId, string TargetName); #pragma warning restore SA1313 - { - public readonly IReadOnlyList Data = []; - - public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr) - : this(opCode, direction, sourceActorId, targetActorId) - => this.Data = MemoryHelper.Read(dataPtr, widget.GetSizeFromOpCode(opCode), false); - } } diff --git a/Dalamud/Plugin/Services/IGameNetwork.cs b/Dalamud/Plugin/Services/IGameNetwork.cs deleted file mode 100644 index 4abf20834..000000000 --- a/Dalamud/Plugin/Services/IGameNetwork.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Dalamud.Game.Network; - -namespace Dalamud.Plugin.Services; - -/// -/// This class handles interacting with game network events. -/// -[Obsolete("Will be removed in a future release. Use packet handler hooks instead.", true)] -public interface IGameNetwork : IDalamudService -{ - // TODO(v9): we shouldn't be passing pointers to the actual data here - - /// - /// The delegate type of a network message event. - /// - /// The pointer to the raw data. - /// The operation ID code. - /// The source actor ID. - /// The taret actor ID. - /// The direction of the packed. - public delegate void OnNetworkMessageDelegate(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction); - - /// - /// Event that is called when a network message is sent/received. - /// - public event OnNetworkMessageDelegate NetworkMessage; -} From 5da79a7dbaa88a3c5695ec54b5882e19f393a5dc Mon Sep 17 00:00:00 2001 From: Limiana <5073202+Limiana@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:38:42 +0300 Subject: [PATCH 03/10] Use RowRef in ZoneInitEventArgs (#2540) * Try-catch packet read * Actually use RowRef instead --- Dalamud/Game/ClientState/ZoneInit.cs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Dalamud/Game/ClientState/ZoneInit.cs b/Dalamud/Game/ClientState/ZoneInit.cs index 7eb4576aa..7d6cda90f 100644 --- a/Dalamud/Game/ClientState/ZoneInit.cs +++ b/Dalamud/Game/ClientState/ZoneInit.cs @@ -3,8 +3,11 @@ using System.Text; using Dalamud.Data; +using Lumina.Excel; using Lumina.Excel.Sheets; +using Serilog; + namespace Dalamud.Game.ClientState; /// @@ -15,7 +18,7 @@ public class ZoneInitEventArgs : EventArgs /// /// Gets the territory type of the zone being entered. /// - public TerritoryType TerritoryType { get; private set; } + public RowRef TerritoryType { get; private set; } /// /// Gets the instance number of the zone, used when multiple copies of an area are active. @@ -25,17 +28,17 @@ public class ZoneInitEventArgs : EventArgs /// /// Gets the associated content finder condition for the zone, if any. /// - public ContentFinderCondition ContentFinderCondition { get; private set; } + public RowRef ContentFinderCondition { get; private set; } /// /// Gets the current weather in the zone upon entry. /// - public Weather Weather { get; private set; } + public RowRef Weather { get; private set; } /// /// Gets the set of active festivals in the zone. /// - public Festival[] ActiveFestivals { get; private set; } = []; + public RowRef[] ActiveFestivals { get; private set; } = []; /// /// Gets the phases corresponding to the active festivals. @@ -54,20 +57,20 @@ public class ZoneInitEventArgs : EventArgs var flags = *(byte*)(packet + 0x12); - eventArgs.TerritoryType = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x02)); + eventArgs.TerritoryType = LuminaUtils.CreateRef(*(ushort*)(packet + 0x02)); eventArgs.Instance = flags >= 0 ? (ushort)0 : *(ushort*)(packet + 0x04); - eventArgs.ContentFinderCondition = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x06)); - eventArgs.Weather = dataManager.GetExcelSheet().GetRow(*(byte*)(packet + 0x10)); + eventArgs.ContentFinderCondition = LuminaUtils.CreateRef(*(ushort*)(packet + 0x06)); + eventArgs.Weather = LuminaUtils.CreateRef(*(byte*)(packet + 0x10)); const int NumFestivals = 8; - eventArgs.ActiveFestivals = new Festival[NumFestivals]; + eventArgs.ActiveFestivals = new RowRef[NumFestivals]; eventArgs.ActiveFestivalPhases = new ushort[NumFestivals]; // There are also 4 festival ids and phases for PlayerState at +0x3E and +0x46 respectively, // but it's unclear why they exist as separate entries and why they would be different. for (var i = 0; i < NumFestivals; i++) { - eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x26 + (i * 2))); + eventArgs.ActiveFestivals[i] = LuminaUtils.CreateRef(*(ushort*)(packet + 0x26 + (i * 2))); eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2)); } From 10ef40ddf5651404470d9da0511fd55c5b4c4f1f Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:42:07 +0100 Subject: [PATCH 04/10] Update ClientStructs (#2595) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 127047085..a02536a4b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 1270470855d6ac2d2f726b07019e21644c5658ec +Subproject commit a02536a4bf6862036403c03945a02fcd6689e445 From c0077b1e260a34e3e2f36ab8d4bd1b08aa9623d2 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 27 Jan 2026 09:30:34 -0800 Subject: [PATCH 05/10] Revert "Use RowRef in ZoneInitEventArgs (#2540)" (#2597) This reverts commit 5da79a7dbaa88a3c5695ec54b5882e19f393a5dc. --- Dalamud/Game/ClientState/ZoneInit.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/ClientState/ZoneInit.cs b/Dalamud/Game/ClientState/ZoneInit.cs index 7d6cda90f..7eb4576aa 100644 --- a/Dalamud/Game/ClientState/ZoneInit.cs +++ b/Dalamud/Game/ClientState/ZoneInit.cs @@ -3,11 +3,8 @@ using System.Text; using Dalamud.Data; -using Lumina.Excel; using Lumina.Excel.Sheets; -using Serilog; - namespace Dalamud.Game.ClientState; /// @@ -18,7 +15,7 @@ public class ZoneInitEventArgs : EventArgs /// /// Gets the territory type of the zone being entered. /// - public RowRef TerritoryType { get; private set; } + public TerritoryType TerritoryType { get; private set; } /// /// Gets the instance number of the zone, used when multiple copies of an area are active. @@ -28,17 +25,17 @@ public class ZoneInitEventArgs : EventArgs /// /// Gets the associated content finder condition for the zone, if any. /// - public RowRef ContentFinderCondition { get; private set; } + public ContentFinderCondition ContentFinderCondition { get; private set; } /// /// Gets the current weather in the zone upon entry. /// - public RowRef Weather { get; private set; } + public Weather Weather { get; private set; } /// /// Gets the set of active festivals in the zone. /// - public RowRef[] ActiveFestivals { get; private set; } = []; + public Festival[] ActiveFestivals { get; private set; } = []; /// /// Gets the phases corresponding to the active festivals. @@ -57,20 +54,20 @@ public class ZoneInitEventArgs : EventArgs var flags = *(byte*)(packet + 0x12); - eventArgs.TerritoryType = LuminaUtils.CreateRef(*(ushort*)(packet + 0x02)); + eventArgs.TerritoryType = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x02)); eventArgs.Instance = flags >= 0 ? (ushort)0 : *(ushort*)(packet + 0x04); - eventArgs.ContentFinderCondition = LuminaUtils.CreateRef(*(ushort*)(packet + 0x06)); - eventArgs.Weather = LuminaUtils.CreateRef(*(byte*)(packet + 0x10)); + eventArgs.ContentFinderCondition = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x06)); + eventArgs.Weather = dataManager.GetExcelSheet().GetRow(*(byte*)(packet + 0x10)); const int NumFestivals = 8; - eventArgs.ActiveFestivals = new RowRef[NumFestivals]; + eventArgs.ActiveFestivals = new Festival[NumFestivals]; eventArgs.ActiveFestivalPhases = new ushort[NumFestivals]; // There are also 4 festival ids and phases for PlayerState at +0x3E and +0x46 respectively, // but it's unclear why they exist as separate entries and why they would be different. for (var i = 0; i < NumFestivals; i++) { - eventArgs.ActiveFestivals[i] = LuminaUtils.CreateRef(*(ushort*)(packet + 0x26 + (i * 2))); + eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x26 + (i * 2))); eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2)); } From e598013e304b993a1ff079325cd62efb69292f23 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 27 Jan 2026 18:53:35 +0100 Subject: [PATCH 06/10] Upgrade goatcorp.Reloaded.Hooks, remove goatcorp.Reloaded.Assembler --- Dalamud/Dalamud.csproj | 1 - Directory.Packages.props | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 287bc5322..a1a08a908 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -65,7 +65,6 @@ - diff --git a/Directory.Packages.props b/Directory.Packages.props index 18760037b..2a8c52dc4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,7 @@ - - + From 5c7a5295d119c65510f9cc0b3fb4b8c4e418ea20 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:49:35 -0800 Subject: [PATCH 07/10] Misc Fixes (#2584) * Disable default logging, remove log message * Add IDtrBarEntry.MinimumWidth * Fix Addon/Agent Lifecycle Register/Unregister * Rename Agent.ReceiveEvent2 * Add to IReadOnlyDtrBarEntry * Fix autoformat being terrible * More style fixes * Add focused changed lifecycle event * Fix for obsolete renames --- .../AddonArgTypes/AddonFocusChangedArgs.cs | 22 ++++ Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 5 + Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 10 ++ .../Game/Addon/Lifecycle/AddonLifecycle.cs | 97 ++++++++++------ .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 34 ++++++ Dalamud/Game/Agent/AgentEvent.cs | 4 +- Dalamud/Game/Agent/AgentLifecycle.cs | 105 +++++++++++------- Dalamud/Game/Agent/AgentVirtualTable.cs | 20 ++-- Dalamud/Game/Gui/Dtr/DtrBar.cs | 10 +- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 29 +++++ 10 files changed, 245 insertions(+), 91 deletions(-) create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFocusChangedArgs.cs diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFocusChangedArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFocusChangedArgs.cs new file mode 100644 index 000000000..8936a233b --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFocusChangedArgs.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for OnFocusChanged events. +/// +public class AddonFocusChangedArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonFocusChangedArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.FocusChanged; + + /// + /// Gets or sets a value indicating whether the window is being focused or unfocused. + /// + public bool ShouldFocus { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 46ee479ac..bc48eeed0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -44,4 +44,9 @@ public enum AddonArgsType /// Contains argument data for Close. /// Close, + + /// + /// Contains argument data for OnFocusChanged. + /// + FocusChanged, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 3b9c6e867..74c84d754 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -203,4 +203,14 @@ public enum AddonEvent /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows. /// PostFocus, + + /// + /// An event that is fired before an addon processes its FocusChanged method. + /// + PreFocusChanged, + + /// + /// An event that is fired after a addon processes its FocusChanged method. + /// + PostFocusChanged, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index c70c0c10f..6520ee4cf 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -31,7 +31,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private readonly Framework framework = Service.Get(); private Hook? onInitializeAddonHook; - private bool isInvokingListeners = false; + private bool isInvokingListeners; [ServiceManager.ServiceConstructor] private AddonLifecycle() @@ -56,29 +56,36 @@ internal unsafe class AddonLifecycle : IInternalDisposableService AllocatedTables.Clear(); } + /// + /// Resolves a virtual table address to the original virtual table address. + /// + /// The modified address to resolve. + /// The original address. + internal static AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress) + { + var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); + if (matchedTable == null) + { + return null; + } + + return matchedTable.OriginalVirtualTable; + } + /// /// Register a listener for the target event and addon. /// /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnTick(() => + if (this.isInvokingListeners) { - 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; - } - - this.EventListeners[listener.EventType][listener.AddonName].Add(listener); - }, delayTicks: this.isInvokingListeners ? 1 : 0); + this.framework.RunOnTick(() => this.RegisterListenerMethod(listener)); + } + else + { + this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener)); + } } /// @@ -87,16 +94,14 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnTick(() => + if (this.isInvokingListeners) { - if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) - { - if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) - { - addonListener.Remove(listener); - } - } - }, delayTicks: this.isInvokingListeners ? 1 : 0); + this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener)); + } + else + { + this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener)); + } } /// @@ -147,17 +152,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.isInvokingListeners = false; } - /// - /// Resolves a virtual table address to the original virtual table address. - /// - /// The modified address to resolve. - /// The original address. - internal AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress) + private void RegisterListenerMethod(AddonLifecycleEventListener listener) { - var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); - if (matchedTable == null) return null; + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + if (!this.EventListeners.TryAdd(listener.EventType, [])) + { + return; + } + } - return matchedTable.OriginalVirtualTable; + // 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); + } + + private void UnregisterListenerMethod(AddonLifecycleEventListener listener) + { + if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) + { + if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + { + addonListener.Remove(listener); + } + } } private void OnAddonInitialize(AtkUnitBase* addon) @@ -277,5 +302,5 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi /// public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress) - => (nint)this.addonLifecycleService.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress); + => (nint)AddonLifecycle.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 736415738..1b2c828f8 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -42,6 +42,7 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonArgs onMouseOverArgs = new(); private readonly AddonArgs onMouseOutArgs = new(); private readonly AddonArgs focusArgs = new(); + private readonly AddonFocusChangedArgs focusChangedArgs = new(); private readonly AtkUnitBase* atkUnitBase; @@ -63,6 +64,7 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction; private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction; private readonly AtkUnitBase.Delegates.Focus focusFunction; + private readonly AtkUnitBase.Delegates.OnFocusChange onFocusChangeFunction; /// /// Initializes a new instance of the class. @@ -103,6 +105,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.onMouseOverFunction = this.OnAddonMouseOver; this.onMouseOutFunction = this.OnAddonMouseOut; this.focusFunction = this.OnAddonFocus; + this.onFocusChangeFunction = this.OnAddonFocusChange; // Overwrite specific virtual table entries this.ModifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); @@ -121,6 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction); this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); this.ModifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); + this.ModifiedVirtualTable->OnFocusChange = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onFocusChangeFunction); } /// @@ -630,6 +634,36 @@ internal unsafe class AddonVirtualTable : IDisposable } } + private void OnAddonFocusChange(AtkUnitBase* thisPtr, bool isFocused) + { + try + { + this.LogEvent(EnableLogging); + + this.focusChangedArgs.Addon = thisPtr; + this.focusChangedArgs.ShouldFocus = isFocused; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocusChanged, this.focusChangedArgs); + + isFocused = this.focusChangedArgs.ShouldFocus; + + try + { + this.OriginalVirtualTable->OnFocusChange(thisPtr, isFocused); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnFocusChanged. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocusChanged, this.focusChangedArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocusChange."); + } + } + [Conditional("DEBUG")] private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") { diff --git a/Dalamud/Game/Agent/AgentEvent.cs b/Dalamud/Game/Agent/AgentEvent.cs index 2a3002daa..e9c9a1b85 100644 --- a/Dalamud/Game/Agent/AgentEvent.cs +++ b/Dalamud/Game/Agent/AgentEvent.cs @@ -18,12 +18,12 @@ public enum AgentEvent /// /// An event that is fired before the agent processes its Filtered Receive Event Function. /// - PreReceiveFilteredEvent, + PreReceiveEventWithResult, /// /// An event that is fired after the agent has processed its Filtered Receive Event Function. /// - PostReceiveFilteredEvent, + PostReceiveEventWithResult, /// /// An event that is fired before the agent processes its Show Function. diff --git a/Dalamud/Game/Agent/AgentLifecycle.cs b/Dalamud/Game/Agent/AgentLifecycle.cs index 75ed47d86..45f0dec5c 100644 --- a/Dalamud/Game/Agent/AgentLifecycle.cs +++ b/Dalamud/Game/Agent/AgentLifecycle.cs @@ -69,30 +69,36 @@ internal unsafe class AgentLifecycle : IInternalDisposableService AllocatedTables.Clear(); } + /// + /// Resolves a virtual table address to the original virtual table address. + /// + /// The modified address to resolve. + /// The original address. + internal static AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress) + { + var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); + if (matchedTable == null) + { + return null; + } + + return matchedTable.OriginalVirtualTable; + } + /// /// Register a listener for the target event and agent. /// /// The listener to register. internal void RegisterListener(AgentLifecycleEventListener listener) { - this.framework.RunOnTick(() => + if (this.isInvokingListeners) { - if (!this.EventListeners.ContainsKey(listener.EventType)) - { - if (!this.EventListeners.TryAdd(listener.EventType, [])) - return; - } - - // Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type - if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId)) - { - if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, [])) - return; - } - - this.EventListeners[listener.EventType][listener.AgentId].Add(listener); - }, - delayTicks: this.isInvokingListeners ? 1 : 0); + this.framework.RunOnTick(() => this.RegisterListenerMethod(listener)); + } + else + { + this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener)); + } } /// @@ -101,17 +107,14 @@ internal unsafe class AgentLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AgentLifecycleEventListener listener) { - this.framework.RunOnTick(() => + if (this.isInvokingListeners) { - if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners)) - { - if (agentListeners.TryGetValue(listener.AgentId, out var agentListener)) - { - agentListener.Remove(listener); - } - } - }, - delayTicks: this.isInvokingListeners ? 1 : 0); + this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener)); + } + else + { + this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener)); + } } /// @@ -162,19 +165,6 @@ internal unsafe class AgentLifecycle : IInternalDisposableService this.isInvokingListeners = false; } - /// - /// Resolves a virtual table address to the original virtual table address. - /// - /// The modified address to resolve. - /// The original address. - internal AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress) - { - var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); - if (matchedTable == null) return null; - - return matchedTable.OriginalVirtualTable; - } - private void OnAgentModuleInitialize(AgentModule* thisPtr, UIModule* uiModule) { this.onInitializeAgentsHook!.Original(thisPtr, uiModule); @@ -193,6 +183,39 @@ internal unsafe class AgentLifecycle : IInternalDisposableService } } + private void RegisterListenerMethod(AgentLifecycleEventListener listener) + { + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + if (!this.EventListeners.TryAdd(listener.EventType, [])) + { + return; + } + } + + // Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId)) + { + if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, [])) + { + return; + } + } + + this.EventListeners[listener.EventType][listener.AgentId].Add(listener); + } + + private void UnregisterListenerMethod(AgentLifecycleEventListener listener) + { + if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners)) + { + if (agentListeners.TryGetValue(listener.AgentId, out var agentListener)) + { + agentListener.Remove(listener); + } + } + } + private void ReplaceVirtualTables(AgentModule* agentModule) { foreach (uint index in Enumerable.Range(0, agentModule->Agents.Length)) @@ -311,5 +334,5 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi /// public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress) - => (nint)this.agentLifecycleService.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress); + => (nint)AgentLifecycle.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress); } diff --git a/Dalamud/Game/Agent/AgentVirtualTable.cs b/Dalamud/Game/Agent/AgentVirtualTable.cs index e7f9a2f6e..99f613137 100644 --- a/Dalamud/Game/Agent/AgentVirtualTable.cs +++ b/Dalamud/Game/Agent/AgentVirtualTable.cs @@ -21,7 +21,7 @@ internal unsafe class AgentVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 60; - private const bool EnableLogging = true; + private const bool EnableLogging = false; private static readonly ModuleLog Log = new("AgentVT"); @@ -44,7 +44,7 @@ internal unsafe class AgentVirtualTable : IDisposable // Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table, // the CLR needs to know they are in use, or it will invalidate them causing random crashing. private readonly AgentInterface.Delegates.ReceiveEvent receiveEventFunction; - private readonly AgentInterface.Delegates.ReceiveEvent2 filteredReceiveEventFunction; + private readonly AgentInterface.Delegates.ReceiveEventWithResult receiveEventWithResultFunction; private readonly AgentInterface.Delegates.Show showFunction; private readonly AgentInterface.Delegates.Hide hideFunction; private readonly AgentInterface.Delegates.Update updateFunction; @@ -60,8 +60,6 @@ internal unsafe class AgentVirtualTable : IDisposable /// Reference to AgentLifecycle service to callback and invoke listeners. internal AgentVirtualTable(AgentInterface* agent, AgentId agentId, AgentLifecycle lifecycleService) { - Log.Debug($"Initializing AgentVirtualTable for {agentId}, Address: {(nint)agent:X}"); - this.agentInterface = agent; this.agentId = agentId; this.lifecycleService = lifecycleService; @@ -80,7 +78,7 @@ internal unsafe class AgentVirtualTable : IDisposable // Pin each of our listener functions this.receiveEventFunction = this.OnAgentReceiveEvent; - this.filteredReceiveEventFunction = this.OnAgentFilteredReceiveEvent; + this.receiveEventWithResultFunction = this.OnAgentReceiveEventWithResult; this.showFunction = this.OnAgentShow; this.hideFunction = this.OnAgentHide; this.updateFunction = this.OnAgentUpdate; @@ -90,7 +88,7 @@ internal unsafe class AgentVirtualTable : IDisposable // Overwrite specific virtual table entries this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction); - this.ModifiedVirtualTable->ReceiveEvent2 = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.filteredReceiveEventFunction); + this.ModifiedVirtualTable->ReceiveEventWithResult = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.receiveEventWithResultFunction); this.ModifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); this.ModifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); this.ModifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); @@ -158,7 +156,7 @@ internal unsafe class AgentVirtualTable : IDisposable return result; } - private AtkValue* OnAgentFilteredReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind) + private AtkValue* OnAgentReceiveEventWithResult(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind) { AtkValue* result = null; @@ -173,7 +171,7 @@ internal unsafe class AgentVirtualTable : IDisposable this.filteredReceiveEventArgs.ValueCount = valueCount; this.filteredReceiveEventArgs.EventKind = eventKind; - this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveFilteredEvent, this.filteredReceiveEventArgs); + this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEventWithResult, this.filteredReceiveEventArgs); returnValue = (AtkValue*)this.filteredReceiveEventArgs.ReturnValue; values = (AtkValue*)this.filteredReceiveEventArgs.AtkValues; @@ -182,18 +180,18 @@ internal unsafe class AgentVirtualTable : IDisposable try { - result = this.OriginalVirtualTable->ReceiveEvent2(thisPtr, returnValue, values, valueCount, eventKind); + result = this.OriginalVirtualTable->ReceiveEventWithResult(thisPtr, returnValue, values, valueCount, eventKind); } catch (Exception e) { Log.Error(e, "Caught exception when calling original Agent FilteredReceiveEvent. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveFilteredEvent, this.filteredReceiveEventArgs); + this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEventWithResult, this.filteredReceiveEventArgs); } catch (Exception e) { - Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentFilteredReceiveEvent."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEventWithResult."); } return result; diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 5663d0748..e5de6b2bd 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -397,7 +397,15 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar ushort w = 0, h = 0; node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); - node->SetWidth(w); + + if (data.MinimumWidth > 0) + { + node->SetWidth(Math.Max(data.MinimumWidth, w)); + } + else + { + node->SetWidth(w); + } } var elementWidth = data.TextNode->Width + this.configuration.DtrSpacing; diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index e0bd8fd49..47e86fde1 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -40,6 +40,11 @@ public interface IReadOnlyDtrBarEntry /// public bool Shown { get; } + /// + /// Gets a value indicating this entry's minimum width. + /// + public ushort MinimumWidth { get; } + /// /// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings. /// @@ -76,6 +81,11 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry /// public new bool Shown { get; set; } + /// + /// Gets or sets a value specifying the requested minimum width to make this entry. + /// + public new ushort MinimumWidth { get; set; } + /// /// Gets or sets an action to be invoked when the user clicks on the dtr entry. /// @@ -128,6 +138,25 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry /// public SeString? Tooltip { get; set; } + /// + public ushort MinimumWidth + { + get; + set + { + field = value; + if (this.TextNode is not null) + { + if (this.TextNode->GetWidth() < value) + { + this.TextNode->SetWidth(value); + } + } + + this.Dirty = true; + } + } + /// public Action? OnClick { get; set; } From 470267a18590157633022ec100b1cfd41cf5cd6c Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 27 Jan 2026 23:17:22 +0100 Subject: [PATCH 08/10] Restore NetworkMessageDirection enum Fixes API breakage --- .../Game/Network/NetworkMessageDirection.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Dalamud/Game/Network/NetworkMessageDirection.cs diff --git a/Dalamud/Game/Network/NetworkMessageDirection.cs b/Dalamud/Game/Network/NetworkMessageDirection.cs new file mode 100644 index 000000000..12cfc3d17 --- /dev/null +++ b/Dalamud/Game/Network/NetworkMessageDirection.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Network; + +/// +/// This represents the direction of a network message. +/// +[Obsolete("No longer part of public API", true)] +public enum NetworkMessageDirection +{ + /// + /// A zone down message. + /// + ZoneDown, + + /// + /// A zone up message. + /// + ZoneUp, +} From 5c250c17254ec89db2aee7ba34afd2925eca3923 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 27 Jan 2026 23:43:51 +0100 Subject: [PATCH 09/10] Make Framework.DelayTicks() deadlock-safe before the game has started --- Dalamud/Game/Framework.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 035745684..e40274043 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -121,9 +121,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework /// public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) { - if (this.frameworkDestroy.IsCancellationRequested) + if (this.frameworkDestroy.IsCancellationRequested) // Going away return Task.FromCanceled(this.frameworkDestroy.Token); - if (numTicks <= 0) + if (numTicks <= 0 || this.frameworkThreadTaskScheduler.BoundThread == null) // Nonsense or before first tick return Task.CompletedTask; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); From 2b51a2a54e20138c9e049b788ffd27b7d0c348be Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 28 Jan 2026 00:35:23 +0100 Subject: [PATCH 10/10] build: 14.0.2.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a1a08a908..34b546faf 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 14.0.1.0 + 14.0.2.0 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion)