diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs
index fcba37310..922b72717 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs
@@ -1,14 +1,14 @@
using System.Collections.Concurrent;
-using System.Linq;
-using System.Text.RegularExpressions;
+using System.Threading;
using Dalamud.Bindings.ImGui;
using Dalamud.Game;
using Dalamud.Hooking;
-using Dalamud.Interface.Utility;
+using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
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;
@@ -27,10 +27,11 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
private bool trackNetwork;
private int trackedPackets = 20;
- private Regex? trackedOpCodes;
+ 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()
@@ -77,6 +78,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
{
if (this.trackNetwork)
{
+ this.nextPacketIndex = 0;
this.hookDown?.Enable();
this.hookUp?.Enable();
}
@@ -87,7 +89,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
}
}
- 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);
@@ -98,12 +100,22 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
this.packets.Clear();
}
- DrawFilterInput("##Filter"u8, "Regex Filter OpCodes..."u8, ref this.filterString, ref this.trackedOpCodes);
- DrawFilterInput("##NegativeFilter"u8, "Regex Filter Against OpCodes..."u8, ref this.negativeFilterString, ref this.untrackedOpCodes);
+ ImGui.SameLine();
+ ImGui.Checkbox("Auto-Scroll"u8, ref this.autoScroll);
- using var table = ImRaii.Table("NetworkMonitorTableV2"u8, 5, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable | ImGuiTableFlags.NoSavedSettings);
+ 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");
+
+ using var table = ImRaii.Table("NetworkMonitorTableV2"u8, 6, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.NoSavedSettings);
if (!table) return;
+ 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);
@@ -112,8 +124,16 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
- foreach (var packet in this.packets.Reverse())
+ var autoScrollDisabled = false;
+
+ foreach (var packet in this.packets)
{
+ if (!this.filterRecording && !this.IsFiltered(packet.OpCode))
+ continue;
+
+ ImGui.TableNextColumn();
+ ImGui.Text(packet.Index.ToString());
+
ImGui.TableNextColumn();
ImGui.Text(packet.Time.ToLongTimeString());
@@ -121,84 +141,88 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
ImGui.Text(packet.Direction.ToString());
ImGui.TableNextColumn();
- WidgetUtil.DrawCopyableText(packet.OpCode.ToString());
-
- ImGui.TableNextColumn();
- WidgetUtil.DrawCopyableText($"0x{packet.OpCode:X4}");
-
- ImGui.TableNextColumn();
- if (packet.TargetActorId > 0)
+ using (ImRaii.PushId(packet.Index.ToString()))
{
- WidgetUtil.DrawCopyableText($"{packet.TargetActorId:X}");
+ if (ImGui.SmallButton("X"))
+ {
+ if (!string.IsNullOrEmpty(this.filterString))
+ this.filterString += ",";
- if (packet.TargetActorId == PlayerState.Instance()->EntityId)
+ 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("(Local Player)");
- }
- else
- {
- var obj = GameObjectManager.Instance()->Objects.GetObjectByEntityId(packet.TargetActorId);
- if (obj != null)
- {
- ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
- ImGui.Text($"({obj->NameString})");
- }
+ ImGui.Text($"({name})");
}
}
}
+
+ if (this.autoScroll && this.autoScrollPending && !autoScrollDisabled)
+ {
+ ImGui.SetScrollHereY();
+ this.autoScrollPending = false;
+ }
}
- private static void DrawFilterInput(ReadOnlySpan label, ReadOnlySpan hint, ref string filterString, ref Regex? regex)
+ private static string GetTargetName(uint targetId)
{
- var invalidRegEx = filterString.Length > 0 && regex == null;
+ if (targetId == PlayerState.Instance()->EntityId)
+ return "Local Player";
- using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx);
- using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx);
+ var cachedName = NameCache.Instance()->GetNameByEntityId(targetId);
+ if (cachedName.HasValue)
+ return cachedName.ToString();
- ImGui.SetNextItemWidth(-1);
- if (!ImGui.InputTextWithHint(label, hint, ref filterString, 1024))
- return;
+ var obj = GameObjectManager.Instance()->Objects.GetObjectByEntityId(targetId);
+ if (obj != null)
+ return obj->NameString;
- if (filterString.Length == 0)
- {
- regex = null;
- return;
- }
-
- try
- {
- regex = new Regex(filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
- }
- catch
- {
- regex = null;
- }
+ return string.Empty;
}
private void OnReceivePacketDetour(PacketDispatcher* thisPtr, uint targetId, nint packet)
{
var opCode = *(ushort*)(packet + 2);
- this.RecordPacket(new NetworkPacketData(DateTime.Now, opCode, NetworkMessageDirection.ZoneDown, targetId));
+ 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(DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0));
+ 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 bool ShouldTrackPacket(ushort opCode)
- {
- return (this.trackedOpCodes == null || this.trackedOpCodes.IsMatch(this.OpCodeToString(opCode)))
- && (this.untrackedOpCodes == null || !this.untrackedOpCodes.IsMatch(this.OpCodeToString(opCode)));
- }
-
private void RecordPacket(NetworkPacketData packet)
{
- if (!this.ShouldTrackPacket(packet.OpCode))
+ if (this.filterRecording && !this.IsFiltered(packet.OpCode))
return;
this.packets.Enqueue(packet);
@@ -207,13 +231,69 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
{
this.packets.TryDequeue(out _);
}
+
+ this.autoScrollPending = true;
}
- /// The filter should find opCodes by number (decimal and hex) and name, if existing.
- private string OpCodeToString(ushort opCode)
- => $"{opCode}\0{opCode:X}";
+ private bool IsFiltered(ushort opcode)
+ {
+ var filterString = this.filterString.Replace(" ", string.Empty);
+
+ if (filterString.Length == 0)
+ return true;
+
+ try
+ {
+ 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;
+ }
+ catch (Exception ex)
+ {
+ Serilog.Log.Error(ex, "Invalid filter string");
+ return false;
+ }
+ }
#pragma warning disable SA1313
- private readonly record struct NetworkPacketData(DateTime Time, ushort OpCode, NetworkMessageDirection Direction, uint TargetActorId);
+ private readonly record struct NetworkPacketData(ulong Index, DateTime Time, ushort OpCode, NetworkMessageDirection Direction, uint TargetEntityId, string TargetName);
#pragma warning restore SA1313
}