diff --git a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs b/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs
index 99c6cb6e9..d7c4eb095 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs
@@ -154,5 +154,10 @@ internal enum DataKind
///
/// Data Share.
///
- DataShare,
+ Data_Share,
+
+ ///
+ /// Network Monitor.
+ ///
+ Network_Monitor,
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
index f392d3912..9d8dc1e93 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
@@ -48,6 +48,7 @@ internal class DataWindow : Window
new DtrBarWidget(),
new UIColorWidget(),
new DataShareWidget(),
+ new NetworkMonitorWidget(),
};
private readonly Dictionary dataKindNames = new();
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs
index 6ec741fe8..ec7124042 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs
@@ -9,7 +9,7 @@ namespace Dalamud.Interface.Internal.Windows.Data;
internal class DataShareWidget : IDataWindowWidget
{
///
- public DataKind DataKind { get; init; } = DataKind.DataShare;
+ public DataKind DataKind { get; init; } = DataKind.Data_Share;
///
public bool Ready { get; set; }
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs
new file mode 100644
index 000000000..ce1559fc8
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text.RegularExpressions;
+
+using Dalamud.Data;
+using Dalamud.Game.Network;
+using Dalamud.Interface.Raii;
+using Dalamud.Memory;
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows.Data;
+
+///
+/// Widget to display the current packets.
+///
+internal class NetworkMonitorWidget : IDataWindowWidget
+{
+ private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId)
+ {
+ public readonly IReadOnlyList Data = Array.Empty();
+
+ 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);
+ }
+
+ private readonly ConcurrentQueue packets = new();
+ private readonly Dictionary opCodeDict = new();
+
+ private bool trackNetwork;
+ private int trackedPackets;
+ private Regex? trackedOpCodes;
+ private string filterString = string.Empty;
+ private Regex? untrackedOpCodes;
+ private string negativeFilterString = string.Empty;
+
+ /// Finalizes an instance of the class.
+ ~NetworkMonitorWidget()
+ {
+ if (this.trackNetwork)
+ {
+ this.trackNetwork = false;
+ var network = Service.GetNullable();
+ if (network != null)
+ {
+ network.NetworkMessage -= this.OnNetworkMessage;
+ }
+ }
+ }
+
+ ///
+ public DataKind DataKind { get; init; } = DataKind.Network_Monitor;
+
+ ///
+ public bool Ready { get; set; }
+
+ ///
+ public void Load()
+ {
+ this.trackNetwork = false;
+ this.trackedPackets = 20;
+ this.trackedOpCodes = null;
+ this.filterString = string.Empty;
+ this.packets.Clear();
+ this.Ready = true;
+ var dataManager = Service.Get();
+ foreach (var (name, code) in dataManager.ClientOpCodes.Concat(dataManager.ServerOpCodes))
+ this.opCodeDict.TryAdd(code, (name, this.GetSizeFromName(name)));
+ }
+
+ ///
+ public void Draw()
+ {
+ var network = Service.Get();
+ if (ImGui.Checkbox("Track Network Packets", ref this.trackNetwork))
+ {
+ if (this.trackNetwork)
+ {
+ network.NetworkMessage += this.OnNetworkMessage;
+ }
+ else
+ {
+ network.NetworkMessage -= this.OnNetworkMessage;
+ }
+ }
+
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2);
+ if (ImGui.DragInt("Stored Number of Packets", ref this.trackedPackets, 0.1f, 1, 512))
+ {
+ this.trackedPackets = Math.Clamp(this.trackedPackets, 1, 512);
+ }
+
+ this.DrawFilterInput();
+ this.DrawNegativeFilterInput();
+
+ ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "Known Name", "OpCode", "Hex", "Target", "Source", "Data");
+ }
+
+ private void DrawNetworkPacket(NetworkPacketData data)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Direction.ToString());
+
+ ImGui.TableNextColumn();
+ if (this.opCodeDict.TryGetValue(data.OpCode, out var pair))
+ {
+ ImGui.TextUnformatted(pair.Name);
+ }
+ else
+ {
+ ImGui.Dummy(new Vector2(150 * ImGuiHelpers.GlobalScale, 0));
+ }
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.OpCode.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted($"0x{data.OpCode:X4}");
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.TargetActorId > 0 ? $"0x{data.TargetActorId:X}" : string.Empty);
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.SourceActorId > 0 ? $"0x{data.SourceActorId:X}" : string.Empty);
+
+ ImGui.TableNextColumn();
+ if (data.Data.Count > 0)
+ {
+ ImGui.TextUnformatted(string.Join(" ", data.Data.Select(b => b.ToString("X2"))));
+ }
+ else
+ {
+ ImGui.Dummy(ImGui.GetContentRegionAvail() with { Y = 0 });
+ }
+ }
+
+ private void DrawFilterInput()
+ {
+ 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", "Regex Filter OpCodes...", ref this.filterString, 1024))
+ {
+ return;
+ }
+
+ if (this.filterString.Length == 0)
+ {
+ this.trackedOpCodes = null;
+ }
+ else
+ {
+ try
+ {
+ this.trackedOpCodes = new Regex(this.filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ }
+ catch
+ {
+ this.trackedOpCodes = null;
+ }
+ }
+ }
+
+ private void DrawNegativeFilterInput()
+ {
+ 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", "Regex Filter Against OpCodes...", ref this.negativeFilterString, 1024))
+ {
+ return;
+ }
+
+ if (this.negativeFilterString.Length == 0)
+ {
+ this.untrackedOpCodes = null;
+ }
+ else
+ {
+ try
+ {
+ this.untrackedOpCodes = new Regex(this.negativeFilterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ }
+ catch
+ {
+ this.untrackedOpCodes = null;
+ }
+ }
+ }
+
+ 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)
+ => this.opCodeDict.TryGetValue(opCode, out var pair) ? pair.Size : 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)
+ => this.opCodeDict.TryGetValue(opCode, out var pair) ? $"{opCode}\0{opCode:X}\0{pair.Name}" : $"{opCode}\0{opCode:X}";
+}