Rework NetworkMonitorWidget, remove GameNetwork

This commit is contained in:
Haselnussbomber 2026-01-21 14:48:11 +01:00
parent 8f8f4faa12
commit 3449762eb3
No known key found for this signature in database
GPG key ID: BB905BB49E7295D1
6 changed files with 119 additions and 332 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,27 +1,32 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Network;
using Dalamud.Game;
using Dalamud.Hooking;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Memory;
using ImGuiTable = Dalamud.Interface.Utility.ImGuiTable;
using FFXIVClientStructs.FFXIV.Application.Network;
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 int trackedPackets = 20;
private Regex? trackedOpCodes;
private string filterString = string.Empty;
private Regex? untrackedOpCodes;
@ -30,15 +35,16 @@ internal class NetworkMonitorWidget : IDataWindowWidget
/// <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,27 +59,31 @@ 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.hookDown?.Enable();
this.hookUp?.Enable();
}
else
{
network.NetworkMessage -= this.OnNetworkMessage;
this.hookDown?.Disable();
this.hookUp?.Disable();
}
}
@ -88,131 +98,122 @@ internal class NetworkMonitorWidget : IDataWindowWidget
this.packets.Clear();
}
this.DrawFilterInput();
this.DrawNegativeFilterInput();
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);
ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "OpCode", "Hex", "Target", "Source", "Data");
}
using var table = ImRaii.Table("NetworkMonitorTableV2"u8, 5, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable | ImGuiTableFlags.NoSavedSettings);
if (!table) return;
private void DrawNetworkPacket(NetworkPacketData data)
{
ImGui.TableNextColumn();
ImGui.Text(data.Direction.ToString());
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(data.OpCode.ToString());
ImGui.TableNextColumn();
ImGui.Text($"0x{data.OpCode:X4}");
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.Reverse())
{
ImGui.Text(string.Join(" ", data.Data.Select(b => b.ToString("X2"))));
}
else
{
ImGui.Dummy(ImGui.GetContentRegionAvail() with { Y = 0 });
ImGui.TableNextColumn();
ImGui.Text(packet.Time.ToLongTimeString());
ImGui.TableNextColumn();
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)
{
WidgetUtil.DrawCopyableText($"{packet.TargetActorId:X}");
if (packet.TargetActorId == PlayerState.Instance()->EntityId)
{
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})");
}
}
}
}
}
private void DrawFilterInput()
private static void DrawFilterInput(ReadOnlySpan<byte> label, ReadOnlySpan<byte> hint, ref string filterString, ref Regex? regex)
{
var invalidRegEx = this.filterString.Length > 0 && this.trackedOpCodes == null;
var invalidRegEx = filterString.Length > 0 && regex == 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))
ImGui.SetNextItemWidth(-1);
if (!ImGui.InputTextWithHint(label, hint, ref filterString, 1024))
return;
if (filterString.Length == 0)
{
regex = null;
return;
}
if (this.filterString.Length == 0)
try
{
this.trackedOpCodes = null;
regex = new Regex(filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
}
else
catch
{
try
{
this.trackedOpCodes = new Regex(this.filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
}
catch
{
this.trackedOpCodes = null;
}
regex = null;
}
}
private void DrawNegativeFilterInput()
private void OnReceivePacketDetour(PacketDispatcher* thisPtr, uint targetId, nint packet)
{
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))
{
var opCode = *(ushort*)(packet + 2);
this.RecordPacket(new NetworkPacketData(DateTime.Now, opCode, NetworkMessageDirection.ZoneDown, targetId));
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));
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))
return;
}
if (this.negativeFilterString.Length == 0)
this.packets.Enqueue(packet);
while (this.packets.Count > this.trackedPackets)
{
this.untrackedOpCodes = null;
}
else
{
try
{
this.untrackedOpCodes = new Regex(this.negativeFilterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
}
catch
{
this.untrackedOpCodes = null;
}
this.packets.TryDequeue(out _);
}
}
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(DateTime Time, ushort OpCode, NetworkMessageDirection Direction, uint TargetActorId);
#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;
}