This commit is contained in:
RoseOfficial 2026-02-20 21:30:42 -05:00 committed by GitHub
commit b1deeaaaff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 830 additions and 49 deletions

View file

@ -0,0 +1,90 @@
using System;
using Dalamud.Game.Network;
using Xunit;
namespace Dalamud.Test.Game.Network;
public class NetworkPointerValidatorTests
{
[Fact]
public void NullPointer_ReturnsFalse()
{
Assert.False(NetworkPointerValidator.IsValidPacketPointer(nint.Zero, 32));
}
[Theory]
[InlineData(0x1)]
[InlineData(0xFF)]
[InlineData(0xFFFF)]
public void BelowMinAddress_ReturnsFalse(long address)
{
Assert.False(NetworkPointerValidator.IsValidPacketPointer((nint)address, 32));
}
[Theory]
[InlineData(0x800000000000)]
[InlineData(0xFFFFFFFFFFFF)]
public void AboveMaxAddress_ReturnsFalse(long address)
{
Assert.False(NetworkPointerValidator.IsValidPacketPointer((nint)address, 32));
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public void NonPositiveSize_ReturnsFalse(int size)
{
Assert.False(NetworkPointerValidator.IsValidPacketPointer((nint)0x10000, size));
}
[Theory]
[InlineData(0x10000, 1)]
[InlineData(0x100000, 1024)]
[InlineData(0x7FFFFFFFFFFF, 1)]
public void ValidPointerAndSize_ReturnsTrue(long address, int size)
{
Assert.True(NetworkPointerValidator.IsValidPacketPointer((nint)address, size));
}
[Fact]
public void SafeRead_InvalidPointer_Throws()
{
Assert.Throws<ArgumentException>(() =>
NetworkPointerValidator.SafeRead<int>(nint.Zero, 0, 32));
}
[Fact]
public void SafeRead_NegativeOffset_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
NetworkPointerValidator.SafeRead<int>((nint)0x10000, -1, 32));
}
[Fact]
public void SafeRead_OffsetExceedsPacket_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
NetworkPointerValidator.SafeRead<int>((nint)0x10000, 30, 32));
}
[Fact]
public void TrySafeRead_InvalidPointer_ReturnsFalse()
{
Assert.False(NetworkPointerValidator.TrySafeRead<int>(nint.Zero, 0, 32, out _));
}
[Fact]
public void TrySafeRead_NegativeOffset_ReturnsFalse()
{
Assert.False(NetworkPointerValidator.TrySafeRead<int>((nint)0x10000, -1, 32, out _));
}
[Fact]
public void TrySafeRead_OffsetExceedsPacket_ReturnsFalse()
{
Assert.False(NetworkPointerValidator.TrySafeRead<int>((nint)0x10000, 30, 32, out _));
}
}

View file

@ -0,0 +1,165 @@
using Dalamud.Interface.Internal.Windows.Data.Widgets;
using Xunit;
namespace Dalamud.Test.Game.Network;
public class OpCodeFilterTests
{
[Fact]
public void EmptyFilter_MatchesAll()
{
Assert.True(NetworkMonitorWidget.IsFiltered(string.Empty, 100));
Assert.True(NetworkMonitorWidget.IsFiltered(string.Empty, 0));
Assert.True(NetworkMonitorWidget.IsFiltered(string.Empty, ushort.MaxValue));
}
[Fact]
public void WhitespaceOnlyFilter_MatchesAll()
{
Assert.True(NetworkMonitorWidget.IsFiltered(" ", 100));
}
[Fact]
public void SingleExactMatch()
{
Assert.True(NetworkMonitorWidget.IsFiltered("100", 100));
}
[Fact]
public void SingleExact_NoMatch()
{
Assert.False(NetworkMonitorWidget.IsFiltered("100", 200));
}
[Fact]
public void CommaSeparatedList_MatchesIncluded()
{
Assert.True(NetworkMonitorWidget.IsFiltered("100,200,300", 200));
}
[Fact]
public void CommaSeparatedList_RejectsExcluded()
{
Assert.False(NetworkMonitorWidget.IsFiltered("100,200,300", 150));
}
[Fact]
public void Range_MatchesWithinBounds()
{
Assert.True(NetworkMonitorWidget.IsFiltered("100-200", 150));
}
[Fact]
public void Range_MatchesLowerBound()
{
Assert.True(NetworkMonitorWidget.IsFiltered("100-200", 100));
}
[Fact]
public void Range_MatchesUpperBound()
{
Assert.True(NetworkMonitorWidget.IsFiltered("100-200", 200));
}
[Fact]
public void Range_RejectsOutside()
{
Assert.False(NetworkMonitorWidget.IsFiltered("100-200", 50));
Assert.False(NetworkMonitorWidget.IsFiltered("100-200", 250));
}
[Fact]
public void OpenEndedRangeStart_MatchesUpTo()
{
// "-400" means everything up to 400
Assert.True(NetworkMonitorWidget.IsFiltered("-400", 0));
Assert.True(NetworkMonitorWidget.IsFiltered("-400", 200));
Assert.True(NetworkMonitorWidget.IsFiltered("-400", 400));
Assert.False(NetworkMonitorWidget.IsFiltered("-400", 401));
}
[Fact]
public void OpenEndedRangeEnd_MatchesFrom()
{
// "700-" means everything from 700 onward
Assert.True(NetworkMonitorWidget.IsFiltered("700-", 700));
Assert.True(NetworkMonitorWidget.IsFiltered("700-", 1000));
Assert.True(NetworkMonitorWidget.IsFiltered("700-", ushort.MaxValue));
Assert.False(NetworkMonitorWidget.IsFiltered("700-", 699));
}
[Fact]
public void Exclusion_ExcludesSingleOpCode()
{
// Only exclusion, no inclusion -> matches everything except excluded
Assert.False(NetworkMonitorWidget.IsFiltered("!50", 50));
Assert.True(NetworkMonitorWidget.IsFiltered("!50", 100));
}
[Fact]
public void Exclusion_ExcludesRange()
{
Assert.False(NetworkMonitorWidget.IsFiltered("!50-100", 75));
Assert.True(NetworkMonitorWidget.IsFiltered("!50-100", 150));
}
[Fact]
public void MixedInclusionAndExclusion()
{
// Include 0-400, but exclude 50-100
var filter = "-400,!50-100";
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 10));
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 75));
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 200));
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 500));
}
[Fact]
public void ComplexFilter()
{
// Example from UI tooltip: -400,!50-100,650,700-980,!941
var filter = "-400,!50-100,650,700-980,!941";
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 10)); // in -400 range
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 75)); // excluded by !50-100
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 300)); // in -400 range
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 500)); // not in any include
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 650)); // exact match
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 700)); // in 700-980
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 800)); // in 700-980
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 941)); // excluded by !941
Assert.True(NetworkMonitorWidget.IsFiltered(filter, 980)); // in 700-980
Assert.False(NetworkMonitorWidget.IsFiltered(filter, 990)); // not in any include
}
[Fact]
public void SpacesAreStripped()
{
Assert.True(NetworkMonitorWidget.IsFiltered(" 100 , 200 ", 100));
Assert.True(NetworkMonitorWidget.IsFiltered(" 100 , 200 ", 200));
Assert.False(NetworkMonitorWidget.IsFiltered(" 100 , 200 ", 150));
}
[Fact]
public void InvalidFilter_ReturnsFalse()
{
Assert.False(NetworkMonitorWidget.IsFiltered("abc", 100));
}
[Fact]
public void ExclusionOnly_MatchesEverythingElse()
{
// No include entries -> treat as "match all except excluded"
Assert.True(NetworkMonitorWidget.IsFiltered("!50,!100", 200));
Assert.False(NetworkMonitorWidget.IsFiltered("!50,!100", 50));
Assert.False(NetworkMonitorWidget.IsFiltered("!50,!100", 100));
}
[Fact]
public void BoundaryValues()
{
Assert.True(NetworkMonitorWidget.IsFiltered("0", 0));
Assert.True(NetworkMonitorWidget.IsFiltered("65535", ushort.MaxValue));
}
}

View file

@ -0,0 +1,234 @@
using System;
using Dalamud.Game.Network;
using Xunit;
namespace Dalamud.Test.Game.Network;
public class SafePacketTests
{
[Fact]
public void ConstructFromByteArray_CopiesData()
{
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
using var packet = new SafePacket(data);
Assert.Equal(4, packet.Size);
Assert.Equal(data, packet.ToArray());
}
[Fact]
public void ConstructFromByteArray_IsolatesFromSource()
{
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
using var packet = new SafePacket(data);
data[0] = 0xFF;
Assert.Equal(0x01, packet.ToArray()[0]);
}
[Fact]
public void ConstructFromNull_Throws()
{
Assert.Throws<ArgumentNullException>(() => new SafePacket(null!));
}
[Fact]
public void ConstructFromEmptyArray_Throws()
{
Assert.Throws<ArgumentException>(() => new SafePacket(Array.Empty<byte>()));
}
[Fact]
public void OpCode_ReadsFirstTwoBytes()
{
var data = new byte[] { 0x34, 0x12, 0x00, 0x00 };
using var packet = new SafePacket(data);
Assert.Equal(0x1234, packet.OpCode);
}
[Fact]
public void OpCode_SingleByte_Throws()
{
var data = new byte[] { 0x01 };
using var packet = new SafePacket(data);
Assert.Throws<InvalidOperationException>(() => packet.OpCode);
}
[Fact]
public void Read_Int32AtOffset()
{
var data = new byte[] { 0x00, 0x00, 0x78, 0x56, 0x34, 0x12 };
using var packet = new SafePacket(data);
Assert.Equal(0x12345678, packet.Read<int>(2));
}
[Fact]
public void Read_NegativeOffset_Throws()
{
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
using var packet = new SafePacket(data);
Assert.Throws<ArgumentOutOfRangeException>(() => packet.Read<int>(-1));
}
[Fact]
public void Read_ExceedsBounds_Throws()
{
var data = new byte[] { 0x01, 0x02 };
using var packet = new SafePacket(data);
Assert.Throws<ArgumentOutOfRangeException>(() => packet.Read<int>(0));
}
[Fact]
public void Read_OffsetAtBoundary_Throws()
{
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
using var packet = new SafePacket(data);
Assert.Throws<ArgumentOutOfRangeException>(() => packet.Read<int>(1));
}
[Fact]
public void TryRead_Success()
{
var data = new byte[] { 0x34, 0x12, 0x00, 0x00 };
using var packet = new SafePacket(data);
Assert.True(packet.TryRead<ushort>(0, out var value));
Assert.Equal(0x1234, value);
}
[Fact]
public void TryRead_OutOfBounds_ReturnsFalse()
{
var data = new byte[] { 0x01, 0x02 };
using var packet = new SafePacket(data);
Assert.False(packet.TryRead<int>(0, out var value));
Assert.Equal(default, value);
}
[Fact]
public void TryRead_NegativeOffset_ReturnsFalse()
{
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
using var packet = new SafePacket(data);
Assert.False(packet.TryRead<int>(-1, out _));
}
[Fact]
public void AsSpan_ReturnsFullData()
{
var data = new byte[] { 0x01, 0x02, 0x03 };
using var packet = new SafePacket(data);
Assert.Equal(data, packet.AsSpan().ToArray());
}
[Fact]
public void AsSpan_WithRange_ReturnsSubset()
{
var data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
using var packet = new SafePacket(data);
var span = packet.AsSpan(1, 3);
Assert.Equal(new byte[] { 0x02, 0x03, 0x04 }, span.ToArray());
}
[Fact]
public void AsSpan_InvalidRange_Throws()
{
var data = new byte[] { 0x01, 0x02 };
using var packet = new SafePacket(data);
Assert.Throws<ArgumentOutOfRangeException>(() => packet.AsSpan(1, 5));
}
[Fact]
public void AsSpan_NegativeOffset_Throws()
{
var data = new byte[] { 0x01, 0x02 };
using var packet = new SafePacket(data);
Assert.Throws<ArgumentOutOfRangeException>(() => packet.AsSpan(-1, 1));
}
[Fact]
public void ToArray_ReturnsIndependentCopy()
{
var data = new byte[] { 0x01, 0x02, 0x03 };
using var packet = new SafePacket(data);
var copy = packet.ToArray();
copy[0] = 0xFF;
Assert.Equal(0x01, packet.ToArray()[0]);
}
[Fact]
public void Dispose_ClearsData()
{
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var packet = new SafePacket(data);
packet.Dispose();
Assert.Throws<ObjectDisposedException>(() => packet.AsSpan());
}
[Fact]
public void Dispose_DoubleDispose_NoThrow()
{
var data = new byte[] { 0x01, 0x02 };
var packet = new SafePacket(data);
packet.Dispose();
packet.Dispose();
}
[Fact]
public void Read_AfterDispose_Throws()
{
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var packet = new SafePacket(data);
packet.Dispose();
Assert.Throws<ObjectDisposedException>(() => packet.Read<int>(0));
}
[Fact]
public void OpCode_AfterDispose_Throws()
{
var data = new byte[] { 0x01, 0x02 };
var packet = new SafePacket(data);
packet.Dispose();
Assert.Throws<ObjectDisposedException>(() => packet.OpCode);
}
[Fact]
public void TryRead_AfterDispose_ReturnsFalse()
{
var data = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var packet = new SafePacket(data);
packet.Dispose();
Assert.False(packet.TryRead<int>(0, out _));
}
[Fact]
public void ToArray_AfterDispose_Throws()
{
var data = new byte[] { 0x01, 0x02 };
var packet = new SafePacket(data);
packet.Dispose();
Assert.Throws<ObjectDisposedException>(() => packet.ToArray());
}
}

View file

@ -0,0 +1,98 @@
using System.Runtime.InteropServices;
namespace Dalamud.Game.Network;
/// <summary>
/// Provides validation utilities for network packet pointers.
/// </summary>
/// <remarks>
/// This is a debug/development utility for validating pointer safety
/// during packet inspection and analysis workflows.
/// </remarks>
internal static class NetworkPointerValidator
{
/// <summary>
/// Minimum address threshold below which pointers are considered invalid.
/// Addresses below this are typically reserved by the OS.
/// </summary>
private const long MinValidAddress = 0x10000;
/// <summary>
/// Maximum valid user-mode address for 64-bit Windows.
/// Addresses above this are kernel-mode and inaccessible from user-mode.
/// </summary>
private const long MaxValidAddress = 0x7FFFFFFFFFFF;
/// <summary>
/// Validates a network packet pointer before use.
/// </summary>
/// <param name="ptr">The pointer to validate.</param>
/// <param name="minSize">The minimum expected size of the data.</param>
/// <returns>True if the pointer appears valid; false otherwise.</returns>
public static bool IsValidPacketPointer(nint ptr, int minSize)
{
if (ptr == nint.Zero)
return false;
if (ptr < MinValidAddress)
return false;
if (ptr > MaxValidAddress)
return false;
if (minSize <= 0)
return false;
return true;
}
/// <summary>
/// Safely reads a value from a packet pointer with bounds checking.
/// </summary>
/// <typeparam name="T">The unmanaged type to read.</typeparam>
/// <param name="ptr">The base pointer to read from.</param>
/// <param name="offset">The byte offset from the base pointer.</param>
/// <param name="packetSize">The total size of the packet for bounds checking.</param>
/// <returns>The value read from memory.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the read would exceed packet boundaries.</exception>
/// <exception cref="ArgumentException">Thrown when the pointer is invalid.</exception>
public static unsafe T SafeRead<T>(nint ptr, int offset, int packetSize) where T : unmanaged
{
if (!IsValidPacketPointer(ptr, packetSize))
throw new ArgumentException("Invalid packet pointer.", nameof(ptr));
var size = sizeof(T);
if (offset < 0 || offset > packetSize || size > packetSize - offset)
{
throw new ArgumentOutOfRangeException(
nameof(offset),
$"Cannot read {size} bytes at offset {offset} from packet of size {packetSize}.");
}
return *(T*)(ptr + offset);
}
/// <summary>
/// Attempts to safely read a value from a packet pointer with bounds checking.
/// </summary>
/// <typeparam name="T">The unmanaged type to read.</typeparam>
/// <param name="ptr">The base pointer to read from.</param>
/// <param name="offset">The byte offset from the base pointer.</param>
/// <param name="packetSize">The total size of the packet for bounds checking.</param>
/// <param name="value">The value read from memory, or default if the read failed.</param>
/// <returns>True if the read succeeded; false otherwise.</returns>
public static unsafe bool TrySafeRead<T>(nint ptr, int offset, int packetSize, out T value) where T : unmanaged
{
value = default;
if (!IsValidPacketPointer(ptr, packetSize))
return false;
var size = sizeof(T);
if (offset < 0 || offset > packetSize || size > packetSize - offset)
return false;
value = *(T*)(ptr + offset);
return true;
}
}

View file

@ -0,0 +1,188 @@
using System.Runtime.InteropServices;
namespace Dalamud.Game.Network;
/// <summary>
/// A safe wrapper around network packet data with lifetime and bounds guarantees.
/// </summary>
/// <remarks>
/// <para>
/// This class copies packet data to managed memory, ensuring:
/// </para>
/// <list type="bullet">
/// <item><description>Lifetime safety - data persists as long as this object.</description></item>
/// <item><description>Bounds checking - all reads are validated against packet size.</description></item>
/// <item><description>Thread safety - the copied data cannot be modified externally.</description></item>
/// </list>
/// <para>
/// Intended for debug tooling and packet inspection utilities.
/// </para>
/// </remarks>
internal sealed class SafePacket : IDisposable
{
private readonly byte[] data;
private bool disposed;
/// <summary>
/// Initializes a new instance of the <see cref="SafePacket"/> class by copying data from an unmanaged pointer.
/// </summary>
/// <param name="ptr">The source pointer to copy from.</param>
/// <param name="size">The number of bytes to copy.</param>
/// <exception cref="ArgumentException">Thrown when the pointer is invalid.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when size is not positive.</exception>
internal SafePacket(nint ptr, int size)
{
if (!NetworkPointerValidator.IsValidPacketPointer(ptr, size))
throw new ArgumentException("Invalid packet pointer.", nameof(ptr));
if (size <= 0)
throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive.");
this.data = new byte[size];
Marshal.Copy(ptr, this.data, 0, size);
this.Size = size;
}
/// <summary>
/// Initializes a new instance of the <see cref="SafePacket"/> class from existing data.
/// </summary>
/// <param name="data">The source data to copy.</param>
/// <exception cref="ArgumentNullException">Thrown when data is null.</exception>
/// <exception cref="ArgumentException">Thrown when data is empty.</exception>
internal SafePacket(byte[] data)
{
ArgumentNullException.ThrowIfNull(data);
if (data.Length == 0)
throw new ArgumentException("Data cannot be empty.", nameof(data));
this.data = new byte[data.Length];
data.CopyTo(this.data, 0);
this.Size = data.Length;
}
/// <summary>
/// Gets the total size of the packet data in bytes.
/// </summary>
public int Size { get; }
/// <summary>
/// Gets the packet opcode (first two bytes interpreted as ushort).
/// </summary>
/// <exception cref="ObjectDisposedException">Thrown when the packet has been disposed.</exception>
/// <exception cref="InvalidOperationException">Thrown when packet is too small to contain an opcode.</exception>
public ushort OpCode
{
get
{
ObjectDisposedException.ThrowIf(this.disposed, this);
if (this.Size < sizeof(ushort))
throw new InvalidOperationException("Packet too small to contain an opcode.");
return BitConverter.ToUInt16(this.data, 0);
}
}
/// <summary>
/// Safely reads a value of type T at the specified byte offset.
/// </summary>
/// <typeparam name="T">The unmanaged type to read.</typeparam>
/// <param name="offset">The byte offset from the start of the packet.</param>
/// <returns>The value read from the packet data.</returns>
/// <exception cref="ObjectDisposedException">Thrown when the packet has been disposed.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the read would exceed packet boundaries.</exception>
public unsafe T Read<T>(int offset) where T : unmanaged
{
ObjectDisposedException.ThrowIf(this.disposed, this);
var size = sizeof(T);
if (offset < 0 || offset > this.Size || size > this.Size - offset)
{
throw new ArgumentOutOfRangeException(
nameof(offset),
$"Cannot read {size} bytes at offset {offset} from packet of size {this.Size}.");
}
return MemoryMarshal.Read<T>(this.data.AsSpan(offset));
}
/// <summary>
/// Attempts to safely read a value of type T at the specified byte offset.
/// </summary>
/// <typeparam name="T">The unmanaged type to read.</typeparam>
/// <param name="offset">The byte offset from the start of the packet.</param>
/// <param name="value">When this method returns, contains the value read, or default if the read failed.</param>
/// <returns>True if the read succeeded; false otherwise.</returns>
public unsafe bool TryRead<T>(int offset, out T value) where T : unmanaged
{
value = default;
if (this.disposed)
return false;
var size = sizeof(T);
if (offset < 0 || offset > this.Size || size > this.Size - offset)
return false;
value = MemoryMarshal.Read<T>(this.data.AsSpan(offset));
return true;
}
/// <summary>
/// Gets a read-only span of the entire packet data.
/// </summary>
/// <returns>A read-only span covering all packet data.</returns>
/// <exception cref="ObjectDisposedException">Thrown when the packet has been disposed.</exception>
public ReadOnlySpan<byte> AsSpan()
{
ObjectDisposedException.ThrowIf(this.disposed, this);
return this.data;
}
/// <summary>
/// Gets a read-only span of a portion of the packet data.
/// </summary>
/// <param name="offset">The starting offset.</param>
/// <param name="length">The number of bytes to include.</param>
/// <returns>A read-only span covering the specified portion of packet data.</returns>
/// <exception cref="ObjectDisposedException">Thrown when the packet has been disposed.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the range exceeds packet boundaries.</exception>
public ReadOnlySpan<byte> AsSpan(int offset, int length)
{
ObjectDisposedException.ThrowIf(this.disposed, this);
if (offset < 0 || length < 0 || offset > this.Size || length > this.Size - offset)
{
throw new ArgumentOutOfRangeException(
nameof(offset),
$"Range [{offset}, {offset + length}) exceeds packet size {this.Size}.");
}
return this.data.AsSpan(offset, length);
}
/// <summary>
/// Creates a copy of the packet data as a new byte array.
/// </summary>
/// <returns>A new byte array containing a copy of the packet data.</returns>
/// <exception cref="ObjectDisposedException">Thrown when the packet has been disposed.</exception>
public byte[] ToArray()
{
ObjectDisposedException.ThrowIf(this.disposed, this);
var copy = new byte[this.Size];
this.data.CopyTo(copy, 0);
return copy;
}
/// <inheritdoc/>
public void Dispose()
{
if (!this.disposed)
{
Array.Clear(this.data);
this.disposed = true;
}
}
}

View file

@ -139,7 +139,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
foreach (var packet in this.packets) foreach (var packet in this.packets)
{ {
if (!this.filterRecording && !this.IsFiltered(packet.OpCode)) if (!this.filterRecording && !IsFiltered(this.filterString, packet.OpCode))
continue; continue;
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -200,55 +200,15 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
} }
} }
private static string GetTargetName(uint targetId) /// <summary>
/// Determines whether a given opcode passes the filter criteria.
/// </summary>
/// <param name="filterString">The comma-separated filter string (e.g. "-400,!50-100,650,700-980,!941").</param>
/// <param name="opcode">The opcode to test.</param>
/// <returns>True if the opcode passes the filter (should be shown/recorded); false otherwise.</returns>
internal static bool IsFiltered(string filterString, ushort opcode)
{ {
if (targetId == PlayerState.Instance()->EntityId) filterString = filterString.Replace(" ", string.Empty);
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.hookZoneDown.OriginalDisposeSafe(thisPtr, targetId, packet);
}
private bool SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, bool a5)
{
var opCode = *(ushort*)packet;
this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty));
return this.hookZoneUp.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 _);
}
this.autoScrollPending = true;
}
private bool IsFiltered(ushort opcode)
{
var filterString = this.filterString.Replace(" ", string.Empty);
if (filterString.Length == 0) if (filterString.Length == 0)
return true; return true;
@ -304,6 +264,52 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
} }
} }
private static string GetTargetName(uint targetId)
{
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.hookZoneDown.OriginalDisposeSafe(thisPtr, targetId, packet);
}
private bool SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, bool a5)
{
var opCode = *(ushort*)packet;
this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty));
return this.hookZoneUp.OriginalDisposeSafe(thisPtr, packet, a3, a4, a5);
}
private void RecordPacket(NetworkPacketData packet)
{
if (this.filterRecording && !IsFiltered(this.filterString, packet.OpCode))
return;
this.packets.Enqueue(packet);
while (this.packets.Count > this.trackedPackets)
{
this.packets.TryDequeue(out _);
}
this.autoScrollPending = true;
}
#pragma warning disable SA1313 #pragma warning disable SA1313
private readonly record struct NetworkPacketData(ulong Index, DateTime Time, ushort OpCode, NetworkMessageDirection Direction, uint TargetEntityId, string TargetName); private readonly record struct NetworkPacketData(ulong Index, DateTime Time, ushort OpCode, NetworkMessageDirection Direction, uint TargetEntityId, string TargetName);
#pragma warning restore SA1313 #pragma warning restore SA1313