Rework NetworkMonitorWidget, remove GameNetwork (#2593)

* Rework NetworkMonitorWidget, remove GameNetwork

* Rework packet filtering
This commit is contained in:
Haselnussbomber 2026-01-27 17:35:55 +01:00 committed by GitHub
parent afa7b0c1f3
commit 3abf7bb00b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 212 additions and 345 deletions

View file

@ -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;
/// <summary>
/// This class handles interacting with game network events.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class GameNetwork : IInternalDisposableService
{
private readonly GameNetworkAddressResolver address;
private readonly Hook<PacketDispatcher.Delegates.OnReceivePacket> processZonePacketDownHook;
private readonly Hook<ProcessZonePacketUpDelegate> processZonePacketUpHook;
private readonly HitchDetector hitchDetectorUp;
private readonly HitchDetector hitchDetectorDown;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.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<PacketDispatcher.Delegates.OnReceivePacket>.FromAddress(onReceivePacketAddress, this.ProcessZonePacketDownDetour);
this.processZonePacketUpHook = Hook<ProcessZonePacketUpDelegate>.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
this.processZonePacketDownHook.Enable();
this.processZonePacketUpHook.Enable();
}
/// <summary>
/// The delegate type of a network message event.
/// </summary>
/// <param name="dataPtr">The pointer to the raw data.</param>
/// <param name="opCode">The operation ID code.</param>
/// <param name="sourceActorId">The source actor ID.</param>
/// <param name="targetActorId">The taret actor ID.</param>
/// <param name="direction">The direction of the packed.</param>
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);
/// <summary>
/// Event that is called when a network message is sent/received.
/// </summary>
public event OnNetworkMessageDelegate? NetworkMessage;
/// <inheritdoc/>
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);
}
}

View file

@ -1,20 +0,0 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Network;
/// <summary>
/// The address resolver for the <see cref="GameNetwork"/> class.
/// </summary>
internal sealed class GameNetworkAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the ProcessZonePacketUp method.
/// </summary>
public IntPtr ProcessZonePacketUp { get; private set; }
/// <inheritdoc/>
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
}
}

View file

@ -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);

View file

@ -1,17 +0,0 @@
namespace Dalamud.Game.Network;
/// <summary>
/// This represents the direction of a network message.
/// </summary>
public enum NetworkMessageDirection
{
/// <summary>
/// A zone down message.
/// </summary>
ZoneDown,
/// <summary>
/// A zone up message.
/// </summary>
ZoneUp,
}

View file

@ -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;
/// <summary>
/// Widget to display the current packets.
/// </summary>
internal class NetworkMonitorWidget : IDataWindowWidget
internal unsafe class NetworkMonitorWidget : IDataWindowWidget
{
private readonly ConcurrentQueue<NetworkPacketData> packets = new();
private Hook<PacketDispatcher.Delegates.OnReceivePacket>? hookDown;
private Hook<ZoneClientSendPacketDelegate>? 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;
/// <summary> Finalizes an instance of the <see cref="NetworkMonitorWidget"/> class. </summary>
~NetworkMonitorWidget()
{
if (this.trackNetwork)
{
this.trackNetwork = false;
var network = Service<GameNetwork>.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,
}
/// <inheritdoc/>
@ -53,31 +60,36 @@ internal class NetworkMonitorWidget : IDataWindowWidget
/// <inheritdoc/>
public void Load()
{
this.trackNetwork = false;
this.trackedPackets = 20;
this.trackedOpCodes = null;
this.filterString = string.Empty;
this.packets.Clear();
this.hookDown = Hook<PacketDispatcher.Delegates.OnReceivePacket>.FromAddress(
(nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket,
this.OnReceivePacketDetour);
// TODO: switch to ZoneClient.SendPacket from CS
if (Service<TargetSigScanner>.Get().TryScanText("E8 ?? ?? ?? ?? 4C 8B 44 24 ?? E9", out var address))
this.hookUp = Hook<ZoneClientSendPacketDelegate>.FromAddress(address, this.SendPacketDetour);
this.Ready = true;
}
/// <inheritdoc/>
public void Draw()
{
var network = Service<GameNetwork>.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;
/// <remarks> Add known packet-name -> packet struct size associations here to copy the byte data for such packets. </remarks>>
private int GetSizeFromName(string name)
=> name switch
{
_ => 0,
};
/// <remarks> The filter should find opCodes by number (decimal and hex) and name, if existing. </remarks>
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<byte> Data = [];
public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr)
: this(opCode, direction, sourceActorId, targetActorId)
=> this.Data = MemoryHelper.Read<byte>(dataPtr, widget.GetSizeFromOpCode(opCode), false);
}
}

View file

@ -1,27 +0,0 @@
using Dalamud.Game.Network;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This class handles interacting with game network events.
/// </summary>
[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
/// <summary>
/// The delegate type of a network message event.
/// </summary>
/// <param name="dataPtr">The pointer to the raw data.</param>
/// <param name="opCode">The operation ID code.</param>
/// <param name="sourceActorId">The source actor ID.</param>
/// <param name="targetActorId">The taret actor ID.</param>
/// <param name="direction">The direction of the packed.</param>
public delegate void OnNetworkMessageDelegate(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction);
/// <summary>
/// Event that is called when a network message is sent/received.
/// </summary>
public event OnNetworkMessageDelegate NetworkMessage;
}