using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using NitroxClient.Unity.Helper; using NitroxModel; using NitroxModel.Packets; using UnityEngine; namespace NitroxClient.Debuggers { [ExcludeFromCodeCoverage] public class NetworkDebugger : BaseDebugger, INetworkDebugger { private const int PACKET_STORED_COUNT = 100; private readonly Dictionary countByType = new Dictionary(); private readonly List filter = new() { nameof(PlayerMovement), nameof(EntityTransformUpdates), nameof(PlayerStats), nameof(SpawnEntities), nameof(VehicleMovements), nameof(PlayerCinematicControllerCall), nameof(FMODAssetPacket), nameof(FMODEventInstancePacket), nameof(FMODCustomEmitterPacket), nameof(FMODStudioEmitterPacket), nameof(FMODCustomLoopingEmitterPacket), nameof(SimulationOwnershipChange), nameof(CellVisibilityChanged), nameof(PlayerInCyclopsMovement), nameof(CreatureActionChanged), nameof(FootstepPacket) }; private readonly List packets = new List(PACKET_STORED_COUNT); // vs blacklist private bool isWhitelist; private Vector2 scrollPosition; private int receivedCount; private int sentCount; private uint receivedBytes; private uint sentBytes; public NetworkDebugger() : base(600, null, KeyCode.N, true, false, false, GUISkinCreationOptions.DERIVEDCOPY, 330) { ActiveTab = AddTab("All", RenderTabPackets); AddTab("Sent", RenderTabSentPackets); AddTab("Received", RenderTabReceivedPackets); AddTab("Type Count", RenderTabTypeCount); AddTab("Filter", RenderTabFilter); } public void PacketSent(Packet packet, int byteSize) { AddPacket(packet, true); sentCount++; sentBytes += (uint)byteSize; } public void PacketReceived(Packet packet, int byteSize) { AddPacket(packet, false); receivedCount++; receivedBytes += (uint)byteSize; } protected override void OnSetSkin(GUISkin skin) { base.OnSetSkin(skin); skin.SetCustomStyle("packet-type-down", skin.label, s => { s.normal = new GUIStyleState { textColor = Color.green }; s.fontStyle = FontStyle.Bold; s.alignment = TextAnchor.MiddleLeft; }); skin.SetCustomStyle("packet-type-up", skin.label, s => { s.normal = new GUIStyleState { textColor = Color.red }; s.fontStyle = FontStyle.Bold; s.alignment = TextAnchor.MiddleLeft; }); } private void RenderTabPackets() { using (new GUILayout.VerticalScope("Box")) { RenderPacketTotals(); scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); RenderPacketList(ToRender.BOTH); GUILayout.EndScrollView(); } } private void RenderTabSentPackets() { using (new GUILayout.VerticalScope("Box")) { RenderPacketTotals(); scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); RenderPacketList(ToRender.SENT); GUILayout.EndScrollView(); } } private void RenderTabReceivedPackets() { using (new GUILayout.VerticalScope("Box")) { RenderPacketTotals(); scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); RenderPacketList(ToRender.RECEIVED); GUILayout.EndScrollView(); } } private void RenderTabTypeCount() { using (new GUILayout.VerticalScope("Box")) { RenderPacketTotals(); scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); foreach (KeyValuePair kv in countByType.OrderBy(e => -e.Value)) // descending { GUILayout.Label($"{kv.Key.Name}: {kv.Value}"); } GUILayout.EndScrollView(); } } private void RenderTabFilter() { using (new GUILayout.VerticalScope("Box")) { RenderPacketTotals(); using (new GUILayout.HorizontalScope()) { isWhitelist = GUILayout.Toggle(isWhitelist, "Is Whitelist"); if (GUILayout.Button("Clear")) { filter.Clear(); } } scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(300)); for (int i = 0; i < filter.Count; i++) { filter[i] = GUILayout.TextField(filter[i]); } string n = GUILayout.TextField(""); if (n != "") { filter.Add(n); } GUILayout.EndScrollView(); } } private void RenderPacketTotals() { GUILayout.Label($"Sent: {sentCount} ({sentBytes.AsByteUnitText()}) - Received: {receivedCount} ({receivedBytes.AsByteUnitText()})"); } private void RenderPacketList(ToRender toRender) { bool isSentList = toRender.HasFlag(ToRender.SENT); bool isReceiveList = toRender.HasFlag(ToRender.RECEIVED); PacketPrefixer prefixer = isSentList && isReceiveList ? (PacketPrefixer)PacketDirectionPrefixer : PacketNoopPrefixer; for (int i = packets.Count - 1; i >= 0; i--) { PacketDebugWrapper wrapper = packets[i]; if (wrapper.IsSent && !isSentList) { continue; } if (!wrapper.IsSent && !isReceiveList) { continue; } using (new GUILayout.VerticalScope("Box")) { using (new GUILayout.HorizontalScope()) { wrapper.ShowDetails = GUILayout.Toggle(wrapper.ShowDetails, "", GUILayout.Width(20), GUILayout.Height(20)); GUILayout.Label($"{prefixer(wrapper)}{wrapper.Packet.GetType().FullName}", wrapper.IsSent ? "packet-type-up" : "packet-type-down"); packets[i] = wrapper; // Store again because value-type } if (wrapper.ShowDetails) { GUILayout.Label(wrapper.Packet.ToString()); } } } } private void AddPacket(Packet packet, bool isSent) { Type packetType = packet.GetType(); if (isWhitelist == filter.Contains(packetType.Name, StringComparer.InvariantCultureIgnoreCase)) { packets.Add(new PacketDebugWrapper(packet, isSent, false)); if (packets.Count > PACKET_STORED_COUNT) { packets.RemoveAt(0); } } if (countByType.TryGetValue(packetType, out int count)) { countByType[packetType] = count + 1; } else { countByType.Add(packetType, 1); } } private string PacketDirectionPrefixer(PacketDebugWrapper wrapper) => $"{(wrapper.IsSent ? "↑" : "↓")} - "; private string PacketNoopPrefixer(PacketDebugWrapper wraper) => ""; private delegate string PacketPrefixer(PacketDebugWrapper wrapper); private struct PacketDebugWrapper { public readonly Packet Packet; public readonly bool IsSent; public bool ShowDetails { get; set; } public PacketDebugWrapper(Packet packet, bool isSent, bool showDetails) { IsSent = isSent; Packet = packet; ShowDetails = showDetails; } } [Flags] private enum ToRender { SENT = 1, RECEIVED = 2, BOTH = SENT | RECEIVED } } public interface INetworkDebugger { void PacketSent(Packet packet, int size); void PacketReceived(Packet packet, int size); } }