diff --git a/.gitignore b/.gitignore
index 350a49dde..f55cb4747 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# Ignore everything:
/Assets/*
!/Assets/FishNet
+!/Assets/FishNet.meta
!.gitignore
DefaultPrefabObjects.asset
diff --git a/.idea/.idea.FishNet/.idea/.gitignore b/.idea/.idea.FishNet/.idea/.gitignore
new file mode 100644
index 000000000..535ce6d11
--- /dev/null
+++ b/.idea/.idea.FishNet/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/projectSettingsUpdater.xml
+/modules.xml
+/contentModel.xml
+/.idea.FishNet.iml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.FishNet/.idea/indexLayout.xml b/.idea/.idea.FishNet/.idea/indexLayout.xml
new file mode 100644
index 000000000..7b08163ce
--- /dev/null
+++ b/.idea/.idea.FishNet/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.FishNet/.idea/vcs.xml b/.idea/.idea.FishNet/.idea/vcs.xml
new file mode 100644
index 000000000..35eb1ddfb
--- /dev/null
+++ b/.idea/.idea.FishNet/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Assets/FishNet.meta b/Assets/FishNet.meta
new file mode 100644
index 000000000..fe2d4d5c7
--- /dev/null
+++ b/Assets/FishNet.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 04c0f770bcb73bc46a75fedc3f97d40d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling.meta b/Assets/FishNet/Runtime/Editor/Profiling.meta
new file mode 100644
index 000000000..1723d1eef
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: bac8d138d5b64f5196d9bc91fcab4110
+timeCreated: 1751232406
\ No newline at end of file
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/ICountRecorderProvider.cs b/Assets/FishNet/Runtime/Editor/Profiling/ICountRecorderProvider.cs
new file mode 100644
index 000000000..303337459
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/ICountRecorderProvider.cs
@@ -0,0 +1,9 @@
+using FishNet.Utility.Performance.Profiling;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI
+{
+ internal interface ICountRecorderProvider
+ {
+ CountRecorder GetCountRecorder();
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/ICountRecorderProvider.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/ICountRecorderProvider.cs.meta
new file mode 100644
index 000000000..9aaffa630
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/ICountRecorderProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e7ff307d04db0b64690d8c9692e40d8c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages.meta b/Assets/FishNet/Runtime/Editor/Profiling/Messages.meta
new file mode 100644
index 000000000..31bc0e207
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 667ec5fd9bee70844a2b458410782b0a
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/Columns.cs b/Assets/FishNet/Runtime/Editor/Profiling/Messages/Columns.cs
new file mode 100644
index 000000000..9b64d381b
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/Columns.cs
@@ -0,0 +1,90 @@
+using System.Collections;
+using System.Collections.Generic;
+using Fishnet.NetworkProfiler.ModuleGUI.UITable;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.Messages
+{
+ internal sealed class Columns : IEnumerable
+ {
+ private const int EXPAND_WIDTH = 25;
+ private const int SHORT_NAME_WIDTH = 75;
+ private const int FULL_NAME_WIDTH = 300;
+ private const int NAME_WIDTH = 450;
+ private const int OTHER_WIDTH = 100;
+
+ private readonly ColumnInfo[] _columns;
+
+ public readonly ColumnInfo Expand;
+ public readonly ColumnInfo FullName;
+ public readonly ColumnInfo TotalBytes;
+ public readonly ColumnInfo Count;
+ public readonly ColumnInfo BytesPerMessage;
+ //public readonly ColumnInfo NetId;
+ public readonly ColumnInfo ObjectName;
+ public readonly ColumnInfo PropertyName;
+
+ public Columns()
+ {
+ Expand = new ColumnInfo("+", EXPAND_WIDTH, x => "");
+
+ FullName = new ColumnInfo("Packet Type", SHORT_NAME_WIDTH, x => x.PacketIdName);
+ FullName.AddSort(m => m.Name, m => m.PacketIdName);
+
+ TotalBytes = new ColumnInfo("Total Bytes", OTHER_WIDTH, x => x.TotalBytes.ToString());
+ TotalBytes.AddSort(m => m.TotalBytes, m => m.TotalBytes);
+
+ Count = new ColumnInfo("Count", OTHER_WIDTH, x => x.Count.ToString());
+ Count.AddSort(m => m.TotalCount, m => m.Count);
+
+ BytesPerMessage = new ColumnInfo("Bytes", OTHER_WIDTH, x => x.Bytes.ToString());
+ BytesPerMessage.AddSort(null, m => m.Bytes);
+
+ // NetId = new ColumnInfo("Net id", OTHER_WIDTH, x => x.NetId.HasValue ? x.NetId.ToString() : "");
+ // NetId.AddSort(null, m => m.NetId.GetValueOrDefault());
+
+ ObjectName = new ColumnInfo("GameObject Name", NAME_WIDTH, x => x.ObjectName);
+ ObjectName.AddSort(null, m => m.ObjectName);
+
+ PropertyName = new ColumnInfo("Prop Name (hover for full name)", FULL_NAME_WIDTH, x => RpcShortName(x.RpcName));
+ PropertyName.AddSort(null, m => m.RpcName);
+ // full name in tooltip
+ PropertyName.AddToolTip(m => m.RpcName);
+
+ _columns = new ColumnInfo[] {
+
+ Expand,
+ FullName,
+ TotalBytes,
+ Count,
+ BytesPerMessage,
+ //NetId,
+ ObjectName,
+ PropertyName,
+ };
+ }
+
+ private string RpcShortName(string fullName)
+ {
+ if (string.IsNullOrEmpty(fullName))
+ return string.Empty;
+
+ const char separator = '.';
+
+ if (fullName.Contains(separator))
+ {
+ var split = fullName.Split(separator);
+ var count = split.Length;
+ if (count >= 2)
+ {
+ return $"{split[count - 2]}.{split[count - 1]}";
+ }
+ }
+
+ return fullName;
+ }
+
+
+ public IEnumerator GetEnumerator() => ((IEnumerable)_columns).GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => _columns.GetEnumerator();
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/Columns.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Messages/Columns.cs.meta
new file mode 100644
index 000000000..f838ed023
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/Columns.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 36dff191a3209c444844bbdc74f60cd4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/CounterNames.cs b/Assets/FishNet/Runtime/Editor/Profiling/Messages/CounterNames.cs
new file mode 100644
index 000000000..32d92c8ae
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/CounterNames.cs
@@ -0,0 +1,16 @@
+namespace Fishnet.NetworkProfiler.ModuleGUI.Messages
+{
+ internal struct CounterNames
+ {
+ public readonly string Count;
+ public readonly string Bytes;
+ public readonly string PerSecond;
+
+ public CounterNames(string count, string bytes, string perSecond)
+ {
+ Count = count;
+ Bytes = bytes;
+ PerSecond = perSecond;
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/CounterNames.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Messages/CounterNames.cs.meta
new file mode 100644
index 000000000..3e8299b61
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/CounterNames.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2422d6a68fe533e4c9da13ae30767fcf
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/DrawnMessage.cs b/Assets/FishNet/Runtime/Editor/Profiling/Messages/DrawnMessage.cs
new file mode 100644
index 000000000..4c3626831
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/DrawnMessage.cs
@@ -0,0 +1,11 @@
+using Fishnet.NetworkProfiler.ModuleGUI.UITable;
+using FishNet.Utility.Performance.Profiling;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.Messages
+{
+ internal class DrawnMessage
+ {
+ public PacketInfo Info;
+ public Row Row;
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/DrawnMessage.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Messages/DrawnMessage.cs.meta
new file mode 100644
index 000000000..2caa2fc0e
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/DrawnMessage.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: df1ae3a59995b1a43b6b76e16d2b8f3a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/Group.cs b/Assets/FishNet/Runtime/Editor/Profiling/Messages/Group.cs
new file mode 100644
index 000000000..d8d51c366
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/Group.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using Fishnet.NetworkProfiler.ModuleGUI.UITable;
+using FishNet.Utility.Performance.Profiling;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.Messages
+{
+ internal class Group
+ {
+ public readonly List Rows = new List();
+ public readonly string Name;
+ public readonly List Messages = new List();
+
+ private readonly Table _table;
+ private readonly Columns _columns;
+
+ public Row Head;
+
+ public int TotalBytes { get; private set; }
+ public int TotalCount { get; private set; }
+ public int Order { get; private set; }
+
+ public bool Expanded { get; private set; }
+
+ public Group(string name, Table table, Columns columns)
+ {
+ Name = name;
+ _table = table;
+ _columns = columns;
+ // start at max, then take min each time message is added
+ Order = int.MaxValue;
+ }
+
+ public void AddMessage(PacketInfo msg)
+ {
+ Messages.Add(new DrawnMessage { Info = msg });
+ TotalBytes += msg.TotalBytes;
+ TotalCount += msg.Count;
+ Order = Math.Min(Order, msg.Order);
+ }
+
+ public void ToggleExpand()
+ {
+ Expand(!Expanded);
+ }
+
+ public void Expand(bool expanded)
+ {
+ Expanded = expanded;
+ // create rows if needed
+ LazyCreateRows();
+ foreach (var row in Rows)
+ {
+ row.VisualElement.style.display = expanded ? DisplayStyle.Flex : DisplayStyle.None;
+ }
+
+ // head can be null if ungrouped
+ Head?.SetText(_columns.Expand, Expanded ? "-" : "+");
+ }
+
+ public void LazyCreateRows()
+ {
+ // not visible, do nothing till row is expanded
+ if (!Expanded)
+ return;
+ // already created
+ if (Rows.Count > 0)
+ return;
+
+ DrawMessages();
+ }
+
+
+ /// Messages to add to table
+ /// list to add rows to once created, Can be null
+ private void DrawMessages()
+ {
+ var previous = Head;
+ var backgroundColor = GetBackgroundColor();
+
+ foreach (var drawn in Messages)
+ {
+ var row = _table.AddRow(previous);
+ Rows.Add(row);
+
+ // set previous to be new row, so that message are added in order after previous
+ previous = row;
+
+ drawn.Row = row;
+ var info = drawn.Info;
+ DrawMessage(row, info);
+
+ // set color of labels not whole row, otherwise color will be outside of table as well
+ foreach (var ele in row.GetChildren())
+ ele.style.backgroundColor = backgroundColor;
+ }
+ }
+
+ private void DrawMessage(Row row, PacketInfo info)
+ {
+ foreach (var column in _columns)
+ {
+ row.SetText(column, column.TextGetter.Invoke(info));
+ if (column.HasToolTip)
+ {
+ var label = row.GetLabel(column);
+ label.tooltip = column.ToolTipGetter.Invoke(info);
+ }
+ }
+ }
+
+ private static Color GetBackgroundColor()
+ {
+ // pick color that is lighter/darker than default editor background
+ // todo check if there is a way to get the real color, or do we have to use `isProSkin`?
+ return EditorGUIUtility.isProSkin
+ ? (Color)new Color32(56, 56, 56, 255) / 0.8f
+ : (Color)new Color32(194, 194, 194, 255) * .8f;
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/Group.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Messages/Group.cs.meta
new file mode 100644
index 000000000..dcc56b888
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/Group.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0d73a0b136989084c8caed7ae1349219
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/GroupSorter.cs b/Assets/FishNet/Runtime/Editor/Profiling/Messages/GroupSorter.cs
new file mode 100644
index 000000000..bd5dcc783
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/GroupSorter.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using Fishnet.NetworkProfiler.ModuleGUI.UITable;
+using FishNet.Utility.Performance.Profiling;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.Messages
+{
+ internal struct GroupSorter
+ {
+ private readonly Dictionary _grouped;
+ private readonly Func _sortGroupFunc;
+ private readonly Func _sortMessageFunc;
+
+ private readonly SortMode _sortMode;
+
+ public GroupSorter(Dictionary grouped, ColumnInfo sortHeader, SortMode sortMode)
+ {
+ _grouped = grouped;
+ _sortMode = sortMode;
+
+ // if header or sort is null, use default
+ _sortGroupFunc = sortHeader?.SortGroup ?? DefaultGroupSort;
+ _sortMessageFunc = sortHeader?.SortMessages ?? DefaultMessageSort;
+ }
+
+ public void Sort()
+ {
+ var groups = new List(_grouped.Values);
+
+ // sort all groups and their messages
+ groups.Sort(CompareGroupSortMode);
+ foreach (var group in groups)
+ {
+ group.Messages.Sort(CompareDrawnSortMode);
+ }
+
+ // apply sort to table
+ foreach (var group in groups)
+ {
+ // use BringToFront so that each new element is placed after the last one, bring them all to their correct position
+
+ // head might be null if messages are ungrouped
+ group.Head?.VisualElement.BringToFront();
+ foreach (var msg in group.Messages)
+ {
+ // row might be null before it is drawn for first time
+ msg.Row?.VisualElement.BringToFront();
+ }
+ }
+ }
+
+ private int CompareGroupSortMode(Group x, Group y)
+ {
+ var sort = _sortGroupFunc.Invoke(x, y);
+ return CheckSortMode(sort);
+ }
+
+ private int CompareDrawnSortMode(DrawnMessage x, DrawnMessage y)
+ {
+ var sort = _sortMessageFunc.Invoke(x.Info, y.Info);
+ return CheckSortMode(sort);
+ }
+
+ private int CheckSortMode(int sort)
+ {
+ // flip order if Descending
+ if (_sortMode == SortMode.Descending)
+ return -sort;
+
+ return sort;
+ }
+
+ public static int Compare(Group x, Group y, Func func) where T : IComparable
+ {
+ var xValue = func.Invoke(x);
+ var yValue = func.Invoke(y);
+ return xValue.CompareTo(yValue);
+ }
+ public static int Compare(PacketInfo x, PacketInfo y, Func func) where T : IComparable
+ {
+ var xValue = func.Invoke(x);
+ var yValue = func.Invoke(y);
+ return xValue.CompareTo(yValue);
+ }
+
+ public static int DefaultGroupSort(Group x, Group y)
+ {
+ return x.Order.CompareTo(y.Order);
+ }
+ public static int DefaultMessageSort(PacketInfo x, PacketInfo y)
+ {
+ return x.Order.CompareTo(y.Order);
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/GroupSorter.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Messages/GroupSorter.cs.meta
new file mode 100644
index 000000000..ecc245ec3
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/GroupSorter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b13bef8f87b23684bae6fc2453c090b0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageView.cs b/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageView.cs
new file mode 100644
index 000000000..d9826c2c1
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageView.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using Fishnet.NetworkProfiler.ModuleGUI.UITable;
+using FishNet.Utility.Performance.Profiling;
+using UnityEngine.UIElements;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.Messages
+{
+ internal class MessageView
+ {
+ private readonly Columns _columns;
+ private readonly Table _table;
+
+ ///
+ /// cache list used to gather messages
+ ///
+ private readonly List _messages = new List();
+ private readonly Dictionary _grouped = new Dictionary();
+
+ public event Action OnGroupExpanded;
+
+ public MessageView(Columns columns, TableSorter sorter, VisualElement parent)
+ {
+ _columns = columns;
+ _table = new Table(columns, sorter);
+ parent.Add(_table.VisualElement);
+ }
+
+ public void Draw(IEnumerable frames, bool groupMessages)
+ {
+ CollectMessages(frames);
+ GroupMessages(groupMessages);
+ DrawGroups(groupMessages);
+
+ var expandColumn = _columns.Expand;
+ var defaultWidth = expandColumn.Width;
+ var width = groupMessages ? defaultWidth : 0;
+ _table.ChangeWidth(expandColumn, width, true);
+ }
+
+ private void CollectMessages(IEnumerable frames)
+ {
+ _messages.Clear();
+ foreach (var frame in frames)
+ {
+ _messages.AddRange(frame.Messages);
+ }
+ }
+
+ private void GroupMessages(bool asGroups)
+ {
+ _grouped.Clear();
+
+ foreach (var message in _messages)
+ {
+ string name;
+ if (asGroups)
+ name = message.PacketIdName;
+ else
+ name = "all_messages";
+
+ if (!_grouped.TryGetValue(name, out var group))
+ {
+ group = new Group(name, _table, _columns);
+ _grouped[name] = group;
+ }
+
+ group.AddMessage(message);
+ }
+ }
+
+ private void DrawGroups(bool withHeader)
+ {
+ foreach (var group in _grouped.Values)
+ {
+ if (withHeader)
+ {
+ DrawGroupHeader(group);
+ }
+ else
+ {
+ group.Expand(true);
+ }
+ }
+ }
+
+ private void DrawGroupHeader(Group group)
+ {
+ // draw header
+ var head = _table.AddRow();
+ head.SetText(_columns.Expand, group.Expanded ? "-" : "+");
+ head.SetText(_columns.FullName, group.Name);
+ head.SetText(_columns.TotalBytes, group.TotalBytes);
+ head.SetText(_columns.Count, group.TotalCount);
+ group.Head = head;
+
+ var expand = head.GetLabel(_columns.Expand);
+ expand.AddManipulator(new Clickable((evt) =>
+ {
+ group.ToggleExpand();
+ OnGroupExpanded?.Invoke(group, group.Expanded);
+ }));
+
+ // will lazy create message if expanded
+ group.Expand(group.Expanded);
+ }
+
+ public void Sort(ColumnInfo sortHeader, SortMode sortMode)
+ {
+ var sorter = new GroupSorter(_grouped, sortHeader, sortMode);
+ sorter.Sort();
+
+ if (sortHeader != null)
+ {
+ // also set table names,
+ _table.SetSortHeader(sortHeader, sortMode);
+ }
+ }
+
+ public void Clear()
+ {
+ _table.Clear();
+ }
+
+ public VisualElement AddEmptyRow()
+ {
+ var row = _table.AddEmptyRow();
+ return row.VisualElement;
+ }
+
+ public void ExpandMany(List expanded)
+ {
+ foreach (var group in _grouped.Values)
+ {
+ if (expanded.Contains(group.Name))
+ {
+ group.Expand(true);
+ }
+ }
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageView.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageView.cs.meta
new file mode 100644
index 000000000..6bfc49b55
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageView.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6f417eeb8a30cca4eb1a9301a73dbf10
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageViewController.cs b/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageViewController.cs
new file mode 100644
index 000000000..5f4676b1c
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageViewController.cs
@@ -0,0 +1,281 @@
+using System;
+using System.Collections.Generic;
+using Fishnet.NetworkProfiler.ModuleGUI.UITable;
+using FishNet.Utility.Performance.Profiling;
+//using Mirage.RemoteCalls;
+using Unity.Profiling;
+using Unity.Profiling.Editor;
+using UnityEditor;
+using UnityEditorInternal;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.Messages
+{
+ internal sealed class MessageViewController : ProfilerModuleViewController
+ {
+ private readonly CounterNames _names;
+ private readonly Columns _columns = new Columns();
+ private Label _countLabel;
+ private Label _bytesLabel;
+ private Label _perSecondLabel;
+ private VisualElement _toggleBox;
+ private Toggle _debugToggle;
+ private Toggle _groupMsgToggle;
+ private MessageView _messageView;
+ private readonly SavedData _savedData;
+
+ public MessageViewController(ProfilerWindow profilerWindow, CounterNames names, SavedData savedData)
+ : base(profilerWindow)
+ {
+ _names = names;
+ _savedData = savedData;
+ }
+
+ protected override VisualElement CreateView()
+ {
+ // unity doesn't catch errors here so we have to wrap in try/catch
+ try
+ {
+ return CreateViewInternal();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ return null;
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!disposing)
+ return;
+
+ // Unsubscribe from the Profiler window event that we previously subscribed to.
+ ProfilerWindow.SelectedFrameIndexChanged -= FrameIndexChanged;
+
+ base.Dispose(disposing);
+ }
+
+ private VisualElement CreateViewInternal()
+ {
+ var root = new VisualElement();
+ root.style.flexDirection = new StyleEnum(FlexDirection.Row);
+ root.style.height = Length.Percent(100);
+ root.style.overflow = Overflow.Hidden;
+
+ var summary = new VisualElement();
+ _countLabel = AddLabelWithPadding(summary);
+ _bytesLabel = AddLabelWithPadding(summary);
+ _perSecondLabel = AddLabelWithPadding(summary);
+ _perSecondLabel.tooltip = Names.PER_SECOND_TOOLTIP;
+ root.Add(summary);
+ summary.style.height = Length.Percent(100);
+ summary.style.width = 180;
+ summary.style.minWidth = 180;
+ summary.style.maxWidth = 180;
+ summary.style.borderRightColor = Color.white * .4f;//dark grey
+ summary.style.borderRightWidth = 3;
+
+ _toggleBox = new VisualElement();
+ _toggleBox.style.position = Position.Absolute;
+ _toggleBox.style.bottom = 5;
+ _toggleBox.style.left = 5;
+ _toggleBox.style.unityTextAlign = TextAnchor.LowerLeft;
+ summary.Add(_toggleBox);
+
+ _groupMsgToggle = new Toggle
+ {
+ text = "Group Messages",
+ tooltip = "Groups Message by type",
+ value = _savedData.GroupMessages,
+ };
+ _groupMsgToggle.RegisterValueChangedCallback(GroupToggled);
+ _toggleBox.Add(_groupMsgToggle);
+
+ // todo allow selection of multiple frames
+ //var frameSlider = new MinMaxSlider();
+ //frameSlider.highLimit = 300;
+ //frameSlider.lowLimit = 1;
+ //frameSlider.value = Vector2.one;
+ //frameSlider.RegisterValueChangedCallback(_ => Debug.Log(frameSlider.value));
+ //_toggleBox.Add(frameSlider);
+
+ _debugToggle = new Toggle
+ {
+ text = "Show Fake Messages",
+ tooltip = "Adds fakes message to table to debug layout of table",
+ value = false
+ };
+ _debugToggle.RegisterValueChangedCallback(_ => ReloadData());
+ _toggleBox.Add(_debugToggle);
+#if MIRAGE_PROFILER_DEBUG
+ _debugToggle.style.display = DisplayStyle.Flex;
+#else
+ _debugToggle.style.display = DisplayStyle.None;
+#endif
+
+
+ var sorter = new TableSorter(this);
+ _messageView = new MessageView(_columns, sorter, root);
+ _messageView.OnGroupExpanded += OnGroupExpanded;
+
+ // Populate the label with the current data for the selected frame.
+ ReloadData();
+
+ // Be notified when the selected frame index in the Profiler Window changes, so we can update the label.
+ ProfilerWindow.SelectedFrameIndexChanged += FrameIndexChanged;
+
+ return root;
+ }
+
+ private void GroupToggled(ChangeEvent evt)
+ {
+ _savedData.GroupMessages = evt.newValue;
+ ReloadData();
+ }
+
+ private void OnGroupExpanded(Group group, bool expanded)
+ {
+ if (expanded)
+ _savedData.Expanded.Add(group.Name);
+ else
+ _savedData.Expanded.Remove(group.Name);
+ }
+
+ private void FrameIndexChanged(long selectedFrameIndex) => ReloadData();
+
+ private static Label AddLabelWithPadding(VisualElement parent)
+ {
+ var label = new Label() { style = { paddingTop = 8, paddingLeft = 8 } };
+ parent.Add(label);
+ return label;
+ }
+
+ internal void Sort(SortHeader header)
+ {
+ _savedData.SetSortHeader(header);
+ SortFromSaveData();
+ }
+
+ private void SortFromSaveData()
+ {
+ var (sortHeader, sortMode) = _savedData.GetSortHeader(_columns);
+ _messageView.Sort(sortHeader, sortMode);
+ }
+
+
+ private void ReloadData()
+ {
+ SetSummary(_countLabel, _names.Count);
+ SetSummary(_bytesLabel, _names.Bytes);
+ SetSummary(_perSecondLabel, _names.PerSecond);
+
+ ReloadMessages();
+ }
+
+ private void SetSummary(Label label, string counterName)
+ {
+ var frame = (int)ProfilerWindow.selectedFrameIndex;
+ var category = ProfilerCategory.Network.Name;
+ var value = ProfilerDriver.GetFormattedCounterValue(frame, category, counterName);
+
+ // replace prefix
+ var display = counterName.Replace("Received", "").Replace("Sent", "").Trim();
+ label.text = $"{display}: {value}";
+ }
+
+ private void ReloadMessages()
+ {
+ const int EditorID = -1;
+
+ _messageView.Clear();
+
+ var frameIndex = (int)ProfilerWindow.selectedFrameIndex;
+ // Debug.Log($"ReloadMessages [selected {(int)ProfilerWindow.selectedFrameIndex}]");
+
+
+ if (ProfilerDriver.connectedProfiler != EditorID)
+ {
+ AddErrorLabel("Can't read message from player");
+ return;
+ }
+
+ if (!TryGetMessages(frameIndex, out var messages))
+ {
+ AddErrorLabel("Can not load messages! (Message list only visible in play mode)\nIMPORTANT: make sure NetworkProfilerBehaviour is setup in starting scene");
+ return;
+ }
+
+ if (messages.Count == 0)
+ {
+ AddInfoLabel("No Messages");
+ return;
+ }
+
+ var frame = new Frame[1] {
+ new Frame{ Messages = messages },
+ };
+ _messageView.Draw(frame, _groupMsgToggle.value);
+ _messageView.ExpandMany(_savedData.Expanded);
+ SortFromSaveData();
+ }
+
+ private bool TryGetMessages(int frameIndex, out List messages)
+ {
+ if (_debugToggle.value)
+ {
+ messages = GenerateDebugMessages();
+ return true;
+ }
+
+ if (frameIndex == -1)
+ {
+ messages = null;
+ return false;
+ }
+
+ messages = _savedData.Frames.GetFrame(frameIndex).Messages;
+ return true;
+ }
+
+ private static List GenerateDebugMessages()
+ {
+ var messages = new List();
+ var order = 0;
+// for (var i = 0; i < 5; i++)
+// {
+// messages.Add(NewInfo(order++, new RpcMessage { NetId = (uint)i }, 20 + i, 5));
+// messages.Add(NewInfo(order++, new SpawnMessage { NetId = (uint)i }, 80 + i, 1));
+// messages.Add(NewInfo(order++, new SpawnMessage { NetId = (uint)i }, 60 + i, 4));
+// messages.Add(NewInfo(order++, new NetworkPingMessage { }, 4, 1));
+
+// static MessageInfo NewInfo(int order, object msg, int bytes, int count)
+// {
+//#if MIRAGE_DIAGNOSTIC_INSTANCE
+// return new MessageInfo(new NetworkDiagnostics.MessageInfo(null, msg, bytes, count), provider, order);
+//#else
+// return new MessageInfo(new NetworkDiagnostics.MessageInfo(msg, bytes, count), new NetworkInfoProvider(null), order);
+//#endif
+// }
+// }
+
+ return messages;
+ }
+
+ private void AddErrorLabel(string message)
+ {
+ var parent = _messageView.AddEmptyRow();
+ var ele = AddLabelWithPadding(parent);
+ ele.style.color = Color.red;
+ ele.text = message;
+ }
+
+ private void AddInfoLabel(string message)
+ {
+ var parent = _messageView.AddEmptyRow();
+ var ele = AddLabelWithPadding(parent);
+ ele.text = message;
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageViewController.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageViewController.cs.meta
new file mode 100644
index 000000000..6a3c59a76
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/MessageViewController.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b210a69331a49414ca5f6436163f61d5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/TableSorter.cs b/Assets/FishNet/Runtime/Editor/Profiling/Messages/TableSorter.cs
new file mode 100644
index 000000000..80da5485b
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/TableSorter.cs
@@ -0,0 +1,27 @@
+using Fishnet.NetworkProfiler.ModuleGUI.UITable;
+using UnityEngine;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.Messages
+{
+
+ internal class TableSorter : ITableSorter
+ {
+ private readonly MessageViewController _controller;
+
+ public TableSorter(MessageViewController controller)
+ {
+ _controller = controller;
+ }
+
+ public void Sort(Table table, SortHeader header)
+ {
+ if (table.ContainsEmptyRows)
+ {
+ Debug.LogWarning("Can't sort when there are empty rows");
+ return;
+ }
+
+ _controller.Sort(header);
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Messages/TableSorter.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Messages/TableSorter.cs.meta
new file mode 100644
index 000000000..6c4c19a03
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Messages/TableSorter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 658b759dbe63e9c44a14d785a15c862e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/ModuleNames.cs b/Assets/FishNet/Runtime/Editor/Profiling/ModuleNames.cs
new file mode 100644
index 000000000..ad149b36c
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/ModuleNames.cs
@@ -0,0 +1,12 @@
+namespace Fishnet.NetworkProfiler.ModuleGUI
+{
+ internal static class ModuleNames
+ {
+ // change this based on netcode lib
+ private const string PREFIX = "FishNet";
+
+ public const string SERVER = PREFIX + " Server";
+ public const string SENT = PREFIX + " Sent";
+ public const string RECEIVED = PREFIX + " Received";
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/ModuleNames.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/ModuleNames.cs.meta
new file mode 100644
index 000000000..eeee9a1a1
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/ModuleNames.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1a65943c66af2cb4cb0433f5f3bd73b7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/ReceivedModule.cs b/Assets/FishNet/Runtime/Editor/Profiling/ReceivedModule.cs
new file mode 100644
index 000000000..fb6e2623c
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/ReceivedModule.cs
@@ -0,0 +1,36 @@
+using Fishnet.NetworkProfiler.ModuleGUI.Messages;
+using FishNet.Utility.Performance.Profiling;
+using Unity.Profiling.Editor;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI
+{
+ [System.Serializable]
+ [ProfilerModuleMetadata(ModuleNames.RECEIVED)]
+ public class ReceivedModule : ProfilerModule, ICountRecorderProvider
+ {
+ private static readonly ProfilerCounterDescriptor[] counters = new ProfilerCounterDescriptor[]
+ {
+ new ProfilerCounterDescriptor(Names.RECEIVED_COUNT, Counters.Category),
+ new ProfilerCounterDescriptor(Names.RECEIVED_BYTES, Counters.Category),
+ new ProfilerCounterDescriptor(Names.RECEIVED_PER_SECOND, Counters.Category),
+ };
+
+ public ReceivedModule() : base(counters) { }
+
+ public override ProfilerModuleViewController CreateDetailsViewController()
+ {
+ var names = new CounterNames(
+ Names.RECEIVED_COUNT,
+ Names.RECEIVED_BYTES,
+ Names.RECEIVED_PER_SECOND
+ );
+
+ return new MessageViewController(ProfilerWindow, names, SaveDataLoader.ReceiveData);
+ }
+
+ CountRecorder ICountRecorderProvider.GetCountRecorder()
+ {
+ return NetworkProfilerRecorder._receivedCounter;
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/ReceivedModule.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/ReceivedModule.cs.meta
new file mode 100644
index 000000000..e5f993026
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/ReceivedModule.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 677abdcefedd3144b9784f7a09a647da
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/SavedData.cs b/Assets/FishNet/Runtime/Editor/Profiling/SavedData.cs
new file mode 100644
index 000000000..f9183d0d6
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/SavedData.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Fishnet.NetworkProfiler.ModuleGUI.Messages;
+using Fishnet.NetworkProfiler.ModuleGUI.UITable;
+using FishNet.Utility.Performance.Profiling;
+using UnityEditor;
+using UnityEngine;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI
+{
+ [Serializable]
+ internal class SavedData
+ {
+ ///
+ /// Message from each frame so they can survive domain reload
+ ///
+ public Frames Frames;
+
+ ///
+ /// Active sort header
+ ///
+ public string SortHeader;
+
+ public SortMode SortMode;
+
+ ///
+ /// Which Message groups are expanded
+ ///
+ public List Expanded;
+
+ public bool GroupMessages = true;
+
+ public SavedData()
+ {
+ Frames = new Frames();
+
+ Expanded = new List();
+ }
+
+ public (ColumnInfo, SortMode) GetSortHeader(Columns columns)
+ {
+ foreach (var c in columns)
+ {
+ if (SortHeader == c.Header)
+ {
+ return (c, SortMode);
+ }
+ }
+
+ return (null, SortMode.None);
+ }
+
+ public void SetSortHeader(SortHeader header)
+ {
+ if (header == null)
+ {
+ SortHeader = "";
+ }
+ else
+ {
+ SortHeader = header.Info.Header;
+ SortMode = header.SortMode;
+ }
+ }
+
+ public void Clear()
+ {
+ foreach (var frame in Frames)
+ {
+ frame.Bytes = 0;
+ frame.Messages.Clear();
+ }
+ }
+ }
+
+ internal class SaveDataLoader
+ {
+ private SavedData _receiveData;
+ private SavedData _sentData;
+ private static SaveDataLoader instance;
+ private static bool isQuitting;
+
+ // private so only we can create one
+ private SaveDataLoader()
+ {
+ NetworkProfilerRecorder.AfterSample += AfterSample;
+ EditorApplication.quitting += Quitting;
+ }
+
+ private void Quitting()
+ {
+ Console.WriteLine("[Mirage.Profiler] quitting");
+ // save and clear references when quitting,
+ // this is needed because finialize is called after unity dll unloads so causes crash
+ SaveBoth();
+ _receiveData = null;
+ _sentData = null;
+ isQuitting = true;
+ }
+
+ ~SaveDataLoader()
+ {
+ NetworkProfilerRecorder.AfterSample -= AfterSample;
+
+ // dont save after quitting, unity might unload their dll and cause crash
+ if (isQuitting)
+ return;
+
+ SaveBoth();
+ }
+
+ private void SaveBoth()
+ {
+ if (_receiveData != null)
+ Save(GetFullPath("Receive"), _receiveData);
+
+ if (_sentData != null)
+ Save(GetFullPath("Sent"), _sentData);
+ }
+
+ private static void AfterSample(int tick)
+ {
+ SetFrame(tick, ReceiveData, NetworkProfilerRecorder._receivedCounter);
+ SetFrame(tick, SentData, NetworkProfilerRecorder._sentCounter);
+ }
+
+ private static void SetFrame(int tick, SavedData data, CountRecorder counter)
+ {
+ var saveFrame = data.Frames.GetFrame(tick);
+
+ // clear old data
+ saveFrame.Bytes = 0;
+ saveFrame.Messages.Clear();
+
+ if (counter == null)
+ return;
+
+ var counterFrame = counter._frames.GetFrame(tick);
+
+ saveFrame.Bytes = counterFrame.Bytes;
+ saveFrame.Messages.AddRange(counterFrame.Messages);
+ }
+
+ public static SavedData ReceiveData
+ {
+ get
+ {
+ // dont load on quit, it might cause crash if unity dll unloads while savedata is save/loading
+ if (isQuitting)
+ return null;
+
+ if (instance == null)
+ instance = new SaveDataLoader();
+
+ if (instance._receiveData == null)
+ {
+ instance._receiveData = Load(GetFullPath("Receive"));
+ }
+ return instance._receiveData;
+ }
+ }
+
+ public static SavedData SentData
+ {
+ get
+ {
+ // dont load on quit, it might cause crash if unity dll unloads while savedata is save/loading
+ if (isQuitting)
+ return null;
+
+ if (instance == null)
+ instance = new SaveDataLoader();
+
+ if (instance._sentData == null)
+ {
+ instance._sentData = Load(GetFullPath("Sent"));
+ }
+ return instance._sentData;
+ }
+ }
+
+ public static string GetFullPath(string name)
+ {
+ var userSettingsFolder = Path.GetFullPath("UserSettings");
+ if (string.IsNullOrEmpty(name))
+ throw new ArgumentNullException(nameof(name));
+
+ return Path.Join(userSettingsFolder, "Mirage.Profiler", $"{name}.json");
+ }
+
+ public static void Save(string path, SavedData data)
+ {
+ Console.WriteLine($"[Mirage.Profiler] Save {path}");
+ CheckDir(path);
+
+ var text = JsonUtility.ToJson(data);
+ File.WriteAllText(path, text);
+ }
+
+ public static SavedData Load(string path)
+ {
+ Console.WriteLine($"[Mirage.Profiler] Load {path}");
+ CheckDir(path);
+
+ if (File.Exists(path))
+ {
+ var text = File.ReadAllText(path);
+ var data = JsonUtility.FromJson(text);
+ Validate(data);
+ return data;
+ }
+ else
+ {
+ return new SavedData();
+ }
+ }
+
+ private static void Validate(SavedData data)
+ {
+ data.Frames.ValidateSize();
+ }
+
+ private static void CheckDir(string path)
+ {
+ // check dir exists
+ var dir = Path.GetDirectoryName(path);
+ if (!Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/SavedData.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/SavedData.cs.meta
new file mode 100644
index 000000000..0198a6cde
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/SavedData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6e3608e18fbb66346bc8d15f628ad4f1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/SentModule.cs b/Assets/FishNet/Runtime/Editor/Profiling/SentModule.cs
new file mode 100644
index 000000000..35807f2ce
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/SentModule.cs
@@ -0,0 +1,36 @@
+using Fishnet.NetworkProfiler.ModuleGUI.Messages;
+using FishNet.Utility.Performance.Profiling;
+using Unity.Profiling.Editor;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI
+{
+ [System.Serializable]
+ [ProfilerModuleMetadata(ModuleNames.SENT)]
+ public class SentModule : ProfilerModule, ICountRecorderProvider
+ {
+ private static readonly ProfilerCounterDescriptor[] counters = new ProfilerCounterDescriptor[]
+ {
+ new ProfilerCounterDescriptor(Names.SENT_COUNT, Counters.Category),
+ new ProfilerCounterDescriptor(Names.SENT_BYTES, Counters.Category),
+ new ProfilerCounterDescriptor(Names.SENT_PER_SECOND, Counters.Category),
+ };
+
+ public SentModule() : base(counters) { }
+
+ public override ProfilerModuleViewController CreateDetailsViewController()
+ {
+ var names = new CounterNames(
+ Names.SENT_COUNT,
+ Names.SENT_BYTES,
+ Names.SENT_PER_SECOND
+ );
+
+ return new MessageViewController(ProfilerWindow, names, SaveDataLoader.SentData);
+ }
+
+ CountRecorder ICountRecorderProvider.GetCountRecorder()
+ {
+ return NetworkProfilerRecorder._sentCounter;
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/SentModule.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/SentModule.cs.meta
new file mode 100644
index 000000000..7dae7f048
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/SentModule.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5447799e8b54b3044b4fac717c450b46
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/ServerModule.cs b/Assets/FishNet/Runtime/Editor/Profiling/ServerModule.cs
new file mode 100644
index 000000000..7924d1653
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/ServerModule.cs
@@ -0,0 +1,109 @@
+using FishNet.Utility.Performance.Profiling;
+using Unity.Profiling;
+using Unity.Profiling.Editor;
+using UnityEditor;
+using UnityEditorInternal;
+using UnityEngine.UIElements;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI
+{
+ [System.Serializable]
+ [ProfilerModuleMetadata(ModuleNames.SERVER)]
+ public class ServerModule : ProfilerModule
+ {
+ private static readonly ProfilerCounterDescriptor[] counters = new ProfilerCounterDescriptor[]
+ {
+ new ProfilerCounterDescriptor(Names.PLAYER_COUNT, Counters.Category),
+ new ProfilerCounterDescriptor(Names.AUTHENTICATED_COUNT, Counters.Category),
+ new ProfilerCounterDescriptor(Names.CHARACTER_COUNT, Counters.Category),
+ new ProfilerCounterDescriptor(Names.OBJECT_COUNT, Counters.Category),
+ };
+
+ public ServerModule() : base(counters) { }
+
+ public override ProfilerModuleViewController CreateDetailsViewController()
+ {
+ return new ServerViewController(ProfilerWindow);
+ }
+ }
+
+ public abstract class BaseViewController : ProfilerModuleViewController
+ {
+ public BaseViewController(ProfilerWindow profilerWindow) : base(profilerWindow) { }
+
+ protected static Label AddLabelWithPadding(VisualElement parent)
+ {
+ var label = new Label() { style = { paddingTop = 8, paddingLeft = 8 } };
+ parent.Add(label);
+ return label;
+ }
+
+
+ protected void SetText(Label label, string name)
+ {
+ var frame = (int)ProfilerWindow.selectedFrameIndex;
+ var category = ProfilerCategory.Network.Name;
+ var value = ProfilerDriver.GetFormattedCounterValue(frame, category, name);
+
+ label.text = $"{name}: {value}";
+ }
+ }
+
+ public sealed class ServerViewController : BaseViewController
+ {
+ private Label PlayerLabel;
+ private Label AuthenticatedLabel;
+ private Label CharacterLabel;
+ private Label ObjectLabel;
+
+ public ServerViewController(ProfilerWindow profilerWindow) : base(profilerWindow) { }
+
+ protected override VisualElement CreateView()
+ {
+ var root = new VisualElement();
+
+ PlayerLabel = AddLabelWithPadding(root);
+ AuthenticatedLabel = AddLabelWithPadding(root);
+ CharacterLabel = AddLabelWithPadding(root);
+ ObjectLabel = AddLabelWithPadding(root);
+
+ PlayerLabel.tooltip = Names.PLAYER_COUNT_TOOLTIP;
+ AuthenticatedLabel.tooltip = Names.AUTHENTICATED_COUNT_TOOLTIP;
+ CharacterLabel.tooltip = Names.CHARACTER_COUNT_TOOLTIP;
+ ObjectLabel.tooltip = Names.OBJECT_COUNT_TOOLTIP;
+
+ // Populate the label with the current data for the selected frame.
+ ReloadData();
+
+ // Be notified when the selected frame index in the Profiler Window changes, so we can update the label.
+ ProfilerWindow.SelectedFrameIndexChanged += OnSelectedFrameIndexChanged;
+
+ return root;
+ }
+
+ private void OnSelectedFrameIndexChanged(long selectedFrameIndex)
+ {
+ // Update the label with the current data for the newly selected frame.
+ ReloadData();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!disposing)
+ return;
+
+ // Unsubscribe from the Profiler window event that we previously subscribed to.
+ ProfilerWindow.SelectedFrameIndexChanged -= OnSelectedFrameIndexChanged;
+
+ base.Dispose(disposing);
+ }
+
+ private void ReloadData()
+ {
+ SetText(PlayerLabel, Names.PLAYER_COUNT);
+ SetText(AuthenticatedLabel, Names.AUTHENTICATED_COUNT);
+ SetText(CharacterLabel, Names.CHARACTER_COUNT);
+ SetText(ObjectLabel, Names.OBJECT_COUNT);
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/ServerModule.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/ServerModule.cs.meta
new file mode 100644
index 000000000..01d46e394
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/ServerModule.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b8113ff8f56cc644bb23d2da87f23eab
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table.meta b/Assets/FishNet/Runtime/Editor/Profiling/Table.meta
new file mode 100644
index 000000000..2dcf6c597
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: cc264da8a8fc533418223d921f2f00c8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/ColumnInfo.cs b/Assets/FishNet/Runtime/Editor/Profiling/Table/ColumnInfo.cs
new file mode 100644
index 000000000..bca7c911a
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/ColumnInfo.cs
@@ -0,0 +1,67 @@
+using System;
+using Fishnet.NetworkProfiler.ModuleGUI.Messages;
+using FishNet.Utility.Performance.Profiling;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.UITable
+{
+ internal class ColumnInfo
+ {
+ public string Header { get; private set; }
+ public int Width { get; private set; }
+
+ public bool AllowSort { get; private set; }
+ public Func SortGroup { get; private set; }
+ public Func SortMessages { get; private set; }
+
+ public Func TextGetter { get; private set; }
+
+ public bool HasToolTip { get; private set; }
+ public Func ToolTipGetter { get; private set; }
+
+ public ColumnInfo(string header, int width, Func textGetter)
+ {
+ Header = header;
+ Width = width;
+ TextGetter = textGetter;
+ }
+
+ ///
+ /// Enables sorting for column. If sort functions are null they will use default sort from
+ ///
+ ///
+ ///
+ ///
+ public void AddSort(Func sortGroup, Func sortMessages)
+ {
+ AllowSort = true;
+ SortGroup = sortGroup ?? GroupSorter.DefaultGroupSort;
+ SortMessages = sortMessages ?? GroupSorter.DefaultMessageSort;
+ }
+
+ ///
+ /// Enables sorting for column. If sort functions are null they will use default sort from
+ /// Uses member getting to sort via that member
+ ///
+ ///
+ ///
+ ///
+ public void AddSort(Func groupGetter, Func messageGetter) where T : IComparable
+ {
+ Func sortGroup = groupGetter != null
+ ? (x, y) => GroupSorter.Compare(x, y, groupGetter)
+ : null;
+
+ Func sortMessages = messageGetter != null
+ ? (x, y) => GroupSorter.Compare(x, y, messageGetter)
+ : null;
+
+ AddSort(sortGroup, sortMessages);
+ }
+
+ public void AddToolTip(Func getter)
+ {
+ HasToolTip = true;
+ ToolTipGetter = getter;
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/ColumnInfo.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Table/ColumnInfo.cs.meta
new file mode 100644
index 000000000..fa911940f
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/ColumnInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7b017b2728f784842bc8f62460ab9fbe
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/EmptyRow.cs b/Assets/FishNet/Runtime/Editor/Profiling/Table/EmptyRow.cs
new file mode 100644
index 000000000..d199e5b72
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/EmptyRow.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine.UIElements;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.UITable
+{
+ internal class EmptyRow : Row
+ {
+ public EmptyRow(Table table, Row previous = null) : base(table, previous) { }
+
+ public override Label GetLabel(ColumnInfo column)
+ {
+ throw new NotSupportedException("Empty row does not have any columns");
+ }
+
+ public override IEnumerable GetChildren()
+ {
+ return Enumerable.Empty();
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/EmptyRow.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Table/EmptyRow.cs.meta
new file mode 100644
index 000000000..b04d5eda2
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/EmptyRow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8129d33c5f799524fa10e84110131e69
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/ITableSorter.cs b/Assets/FishNet/Runtime/Editor/Profiling/Table/ITableSorter.cs
new file mode 100644
index 000000000..1aa026870
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/ITableSorter.cs
@@ -0,0 +1,7 @@
+namespace Fishnet.NetworkProfiler.ModuleGUI.UITable
+{
+ internal interface ITableSorter
+ {
+ void Sort(Table table, SortHeader header);
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/ITableSorter.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Table/ITableSorter.cs.meta
new file mode 100644
index 000000000..ed3d524e5
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/ITableSorter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 05c177a4f382419409a2f55de20e046c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/LabelRow.cs b/Assets/FishNet/Runtime/Editor/Profiling/Table/LabelRow.cs
new file mode 100644
index 000000000..3bb73e42d
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/LabelRow.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.UITable
+{
+ internal class LabelRow : Row
+ {
+ private readonly Dictionary _elements = new Dictionary();
+
+ public LabelRow(Table table, Row previous = null) : base(table, previous)
+ {
+ foreach (var header in table.HeaderInfo)
+ {
+ var label = CreateLabel(header);
+ VisualElement.Add(label);
+ _elements[header] = label;
+ }
+ }
+
+ protected virtual Label CreateLabel(ColumnInfo column)
+ {
+ var label = new Label();
+ SetLabelStyle(column, label);
+ return label;
+ }
+
+ protected static void SetLabelStyle(ColumnInfo column, Label label)
+ {
+ var style = label.style;
+ style.width = column.Width;
+
+ style.paddingLeft = 5;
+ style.paddingRight = 5;
+ style.paddingTop = 5;
+ style.paddingBottom = 5;
+ style.borderRightColor = Color.white * .4f;
+ style.borderBottomColor = Color.white * .4f;
+ style.borderBottomWidth = 1;
+ style.borderRightWidth = 2;
+
+ style.overflow = Overflow.Hidden;
+ style.textOverflow = TextOverflow.Ellipsis;
+ }
+
+ public override Label GetLabel(ColumnInfo column)
+ {
+ return _elements[column];
+ }
+
+ public override IEnumerable GetChildren()
+ {
+ return _elements.Values;
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/LabelRow.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Table/LabelRow.cs.meta
new file mode 100644
index 000000000..7cc1ace4d
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/LabelRow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e463bd968d1bb0e4cbae5efd09ed24fc
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/Row.cs b/Assets/FishNet/Runtime/Editor/Profiling/Table/Row.cs
new file mode 100644
index 000000000..12a26579a
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/Row.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using UnityEngine.UIElements;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.UITable
+{
+ internal abstract class Row
+ {
+ public Table Table { get; }
+ public VisualElement VisualElement { get; }
+
+ public Row(Table table, Row previous = null)
+ {
+ Table = table;
+
+ VisualElement = new VisualElement();
+ VisualElement.style.flexDirection = FlexDirection.Row;
+
+ var parent = table.ScrollView;
+ if (previous != null)
+ {
+ var index = parent.IndexOf(previous.VisualElement);
+ // insert after previous
+ parent.Insert(index + 1, VisualElement);
+ }
+ else
+ {
+ // just add at end
+ parent.Add(VisualElement);
+ }
+ }
+
+ public abstract Label GetLabel(ColumnInfo column);
+ public abstract IEnumerable GetChildren();
+
+ public void SetText(ColumnInfo column, object obj)
+ {
+ SetText(column, obj.ToString());
+ }
+ public void SetText(ColumnInfo column, string text)
+ {
+ var label = GetLabel(column);
+ label.text = text;
+ label.tooltip = text;
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/Row.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Table/Row.cs.meta
new file mode 100644
index 000000000..98b930f5b
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/Row.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ac8d50f233b70db42a649ca6b7d69010
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeader.cs b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeader.cs
new file mode 100644
index 000000000..8f6e7dbb3
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeader.cs
@@ -0,0 +1,63 @@
+using UnityEngine.UIElements;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.UITable
+{
+ internal class SortHeader : Label
+ {
+ public const string ARROW_UP = "\u2191";
+ public const string ARROW_DOWN = "\u2193";
+
+ public SortMode SortMode;
+
+ private readonly SortHeaderRow _row;
+
+ private string _nameWithoutSort;
+ public string NameWithoutSort
+ {
+ get
+ {
+ // this get should be called before any modifications are made
+ // so lazy property should be safe here
+ if (_nameWithoutSort == null)
+ _nameWithoutSort = text;
+
+ return _nameWithoutSort;
+ }
+ }
+
+ public ColumnInfo Info { get; internal set; }
+
+ public SortHeader(SortHeaderRow row) : base()
+ {
+ _row = row;
+ this.AddManipulator(new Clickable(OnClick));
+ }
+
+ private void OnClick(EventBase evt)
+ {
+ // just flip between Accending and Descending, no going back to none
+ if (SortMode == SortMode.Accending)
+ SortMode = SortMode.Descending;
+ else
+ SortMode = SortMode.Accending;
+
+ _row.ApplySort(this);
+ }
+
+ public void UpdateName()
+ {
+ switch (SortMode)
+ {
+ case SortMode.None:
+ text = NameWithoutSort;
+ break;
+ case SortMode.Accending:
+ text = ARROW_UP + NameWithoutSort;
+ break;
+ case SortMode.Descending:
+ text = ARROW_DOWN + NameWithoutSort;
+ break;
+ }
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeader.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeader.cs.meta
new file mode 100644
index 000000000..913308b1d
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeader.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ab266974aca81aa4abbaefba1e934073
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeaderRow.cs b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeaderRow.cs
new file mode 100644
index 000000000..23329ad07
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeaderRow.cs
@@ -0,0 +1,54 @@
+using System;
+using UnityEngine.UIElements;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.UITable
+{
+ internal class SortHeaderRow : LabelRow
+ {
+ private readonly ITableSorter _sorter;
+ private SortHeader _currentSort;
+
+ public SortHeaderRow(Table table, ITableSorter sorter) : base(table, null)
+ {
+ _sorter = sorter ?? throw new ArgumentNullException(nameof(sorter));
+ }
+
+ protected override Label CreateLabel(ColumnInfo column)
+ {
+ var label = column.AllowSort
+ ? new SortHeader(this)
+ : new Label();
+
+ SetLabelStyle(column, label);
+ return label;
+ }
+
+ ///
+ /// Update names of header based on sort
+ ///
+ ///
+ public void SetSortHeader(SortHeader sortHeader)
+ {
+ // not null or current
+ if (_currentSort != null && _currentSort != sortHeader)
+ {
+ _currentSort.SortMode = SortMode.None;
+ _currentSort.UpdateName();
+ }
+
+ _currentSort = sortHeader;
+ _currentSort.UpdateName();
+ }
+
+ ///
+ /// Updates names and applies sort to table
+ ///
+ ///
+ public void ApplySort(SortHeader sortHeader)
+ {
+ SetSortHeader(sortHeader);
+
+ _sorter.Sort(Table, sortHeader);
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeaderRow.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeaderRow.cs.meta
new file mode 100644
index 000000000..70290d53f
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortHeaderRow.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4b4301d9972553c4ab486e9b8915f784
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/SortMode.cs b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortMode.cs
new file mode 100644
index 000000000..40ebabf95
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortMode.cs
@@ -0,0 +1,9 @@
+namespace Fishnet.NetworkProfiler.ModuleGUI.UITable
+{
+ internal enum SortMode
+ {
+ None,
+ Accending,
+ Descending,
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/SortMode.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortMode.cs.meta
new file mode 100644
index 000000000..8f77f1cd6
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/SortMode.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f0d80f37bc0b1a44cbad7667bba04c79
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/Table.cs b/Assets/FishNet/Runtime/Editor/Profiling/Table/Table.cs
new file mode 100644
index 000000000..3da599ce1
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/Table.cs
@@ -0,0 +1,113 @@
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace Fishnet.NetworkProfiler.ModuleGUI.UITable
+{
+ internal class Table
+ {
+ public readonly VisualElement VisualElement;
+ public readonly ScrollView ScrollView;
+
+ public readonly SortHeaderRow Header;
+ public readonly List Rows = new List();
+ public readonly IReadOnlyList HeaderInfo;
+
+ public bool ContainsEmptyRows { get; private set; }
+
+ public Table(IEnumerable columns, ITableSorter sorter)
+ {
+ // create readonly list from given Enumerable
+ HeaderInfo = new List(columns);
+
+ // create table root and scroll view
+ // root can be Horizontal scroll
+ // Horizontal will also move header, but Vertical keeps header at top
+ VisualElement = new ScrollView(ScrollViewMode.Horizontal);
+ // seperate root for contents, so that Horizontal from scroll isn't applied
+ var root = new VisualElement();
+ VisualElement.Add(root);
+ // using VerticalAndHorizontal fixes header being squashed, not sure why, or if it'll cause future problems
+ ScrollView = new ScrollView(ScrollViewMode.VerticalAndHorizontal);
+
+ // add header to table root
+ // header will initialize labels, but we need to set text
+ Header = new SortHeaderRow(this, sorter);
+
+ // add header and scroll to root
+ root.Add(Header.VisualElement);
+ root.Add(ScrollView);
+
+ // add headers
+ foreach (var c in columns)
+ {
+ var ele = Header.GetLabel(c);
+ ele.text = c.Header;
+
+ if (c.AllowSort)
+ {
+ var sortHeader = (SortHeader)ele;
+ sortHeader.Info = c;
+ }
+
+ // make header element thicker
+ var eleStyle = ele.style;
+ eleStyle.unityFontStyleAndWeight = FontStyle.Bold;
+ eleStyle.borderBottomWidth = 3;
+ eleStyle.borderRightWidth = 3;
+ }
+ }
+
+ public Row AddRow(Row previous = null)
+ {
+ var row = new LabelRow(this, previous);
+ Rows.Add(row);
+ return row;
+ }
+
+ public Row AddEmptyRow(Row previous = null)
+ {
+ var row = new EmptyRow(this, previous);
+ ContainsEmptyRows = true;
+ Rows.Add(row);
+ return row;
+ }
+
+ public void ChangeWidth(ColumnInfo column, int newWidth, bool setVisibility)
+ {
+ foreach (var row in Rows)
+ {
+ if (row is EmptyRow)
+ continue;
+
+ var label = row.GetLabel(column);
+ var style = label.style;
+ style.width = newWidth;
+
+ if (setVisibility)
+ style.display = newWidth > 0 ? DisplayStyle.Flex : DisplayStyle.None;
+ }
+ }
+
+ ///
+ /// Removes all rows expect header
+ ///
+ public void Clear()
+ {
+ ScrollView.Clear();
+ Rows.Clear();
+ Rows.Add(Header);
+ ContainsEmptyRows = false;
+ }
+
+ ///
+ /// Updates names of sort header
+ ///
+ public void SetSortHeader(ColumnInfo info, SortMode mode)
+ {
+ var sortBy = (SortHeader)Header.GetLabel(info);
+ sortBy.SortMode = mode;
+ Header.SetSortHeader(sortBy);
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Editor/Profiling/Table/Table.cs.meta b/Assets/FishNet/Runtime/Editor/Profiling/Table/Table.cs.meta
new file mode 100644
index 000000000..46d81dd7d
--- /dev/null
+++ b/Assets/FishNet/Runtime/Editor/Profiling/Table/Table.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2a7346de93910944c965b68a39d93082
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/FishNet.Runtime.asmdef b/Assets/FishNet/Runtime/FishNet.Runtime.asmdef
index 73a8b851e..898636b60 100644
--- a/Assets/FishNet/Runtime/FishNet.Runtime.asmdef
+++ b/Assets/FishNet/Runtime/FishNet.Runtime.asmdef
@@ -1,8 +1,10 @@
{
"name": "FishNet.Runtime",
+ "rootNamespace": "",
"references": [
"GUID:894a6cc6ed5cd2645bb542978cbed6a9",
- "GUID:17013b2a21298c14ea4808251346a38a"
+ "GUID:17013b2a21298c14ea4808251346a38a",
+ "GUID:b46779583a009f04ba9f5f31d0e7e6ac"
],
"includePlatforms": [],
"excludePlatforms": [],
diff --git a/Assets/FishNet/Runtime/Managing/Client/ClientManager.cs b/Assets/FishNet/Runtime/Managing/Client/ClientManager.cs
index cdd172db1..94a710b21 100644
--- a/Assets/FishNet/Runtime/Managing/Client/ClientManager.cs
+++ b/Assets/FishNet/Runtime/Managing/Client/ClientManager.cs
@@ -46,6 +46,10 @@ public sealed partial class ClientManager : MonoBehaviour
///
public event Action OnConnectedClients;
///
+ /// Called when the client processes a packet.
+ ///
+ public event Action OnPacketRead;
+ ///
/// True if the client connection is connected to the server.
///
public bool Started { get; private set; }
@@ -152,6 +156,7 @@ internal void InitializeOnce_Internal(NetworkManager manager)
NetworkManager = manager;
Objects = new ClientObjects(manager);
Objects.SubscribeToSceneLoaded(true);
+ Objects.OnPacketRead += (args) => OnPacketRead?.Invoke(args);
/* Unsubscribe before subscribing.
* Shouldn't be an issue but better safe than sorry. */
SubscribeToEvents(false);
@@ -400,6 +405,7 @@ internal void ParseReader(PooledReader reader, Channel channel, bool print = fal
reader.Initialize(NetworkManager.TransportManager.ProcessIntermediateIncoming(fullMessage, true), NetworkManager, dataSource);
else
reader.Initialize(fullMessage, NetworkManager, dataSource);
+ OnPacketRead?.Invoke(new PacketProcessingArgs(null, -1, PacketId.Split, expectedMessages*1500));
}
//Not split.
else
diff --git a/Assets/FishNet/Runtime/Managing/Client/Object/ClientObjects.cs b/Assets/FishNet/Runtime/Managing/Client/Object/ClientObjects.cs
index bc0a96d64..cdc8a6da9 100644
--- a/Assets/FishNet/Runtime/Managing/Client/Object/ClientObjects.cs
+++ b/Assets/FishNet/Runtime/Managing/Client/Object/ClientObjects.cs
@@ -32,6 +32,11 @@ public partial class ClientObjects : ManagedObjects
/// NetworkObjects which are currently active on the local client.
///
internal List LocalClientSpawned = new List();
+
+ ///
+ /// Called when the client processes a packet.
+ ///
+ public event Action OnPacketRead;
#endregion
#region Private.
@@ -328,6 +333,7 @@ internal void ParseSyncType(PooledReader reader, bool isSyncObject, Channel chan
if (nb != null)
{
+ OnPacketRead?.Invoke(new PacketProcessingArgs(nb, (int)reader.PeekByte(), (PacketId)packetId, dataLength));
/* Length of data to be read for syncvars.
* This is important because syncvars are never
* a set length and data must be read through completion.
@@ -369,7 +375,10 @@ internal void ParseReconcileRpc(PooledReader reader, Channel channel)
int dataLength = Packets.GetPacketLength((ushort)PacketId.Reconcile, reader, channel);
if (nb != null)
+ {
+ OnPacketRead?.Invoke(new PacketProcessingArgs(nb, (int)nb.PeekRpcHash(reader), PacketId.Reconcile, dataLength));
nb.OnReconcileRpc(null, reader, channel);
+ }
else
SkipDataLength((ushort)PacketId.ObserversRpc, reader, dataLength);
}
@@ -385,7 +394,10 @@ internal void ParseObserversRpc(PooledReader reader, Channel channel)
int dataLength = Packets.GetPacketLength((ushort)PacketId.ObserversRpc, reader, channel);
if (nb != null)
+ {
+ OnPacketRead?.Invoke(new PacketProcessingArgs(nb, (int)nb.PeekRpcHash(reader), PacketId.ObserversRpc, dataLength));
nb.OnObserversRpc(null, reader, channel);
+ }
else
SkipDataLength((ushort)PacketId.ObserversRpc, reader, dataLength);
}
@@ -400,7 +412,10 @@ internal void ParseTargetRpc(PooledReader reader, Channel channel)
int dataLength = Packets.GetPacketLength((ushort)PacketId.TargetRpc, reader, channel);
if (nb != null)
+ {
+ OnPacketRead?.Invoke(new PacketProcessingArgs(nb, (int)nb.PeekRpcHash(reader), PacketId.TargetRpc, dataLength));
nb.OnTargetRpc(null, reader, channel);
+ }
else
SkipDataLength((ushort)PacketId.TargetRpc, reader, dataLength);
}
diff --git a/Assets/FishNet/Runtime/Object/NetworkBehaviour.RPCs.cs b/Assets/FishNet/Runtime/Object/NetworkBehaviour.RPCs.cs
index 995ff30b1..27b363e10 100644
--- a/Assets/FishNet/Runtime/Object/NetworkBehaviour.RPCs.cs
+++ b/Assets/FishNet/Runtime/Object/NetworkBehaviour.RPCs.cs
@@ -165,6 +165,19 @@ public void ClearBuffedRpcs()
bRpc.Writer.Store();
_bufferedRpcs.Clear();
}
+
+ ///
+ /// Reads a RPC hash.
+ ///
+ ///
+ ///
+ internal uint PeekRpcHash(PooledReader reader)
+ {
+ if (_rpcHashSize == 1)
+ return reader.PeekByte();
+ else
+ return reader.PeekUInt16();
+ }
///
/// Reads a RPC hash.
diff --git a/Assets/FishNet/Runtime/Serializing/Reader.cs b/Assets/FishNet/Runtime/Serializing/Reader.cs
index 077d83acd..131e18dd1 100644
--- a/Assets/FishNet/Runtime/Serializing/Reader.cs
+++ b/Assets/FishNet/Runtime/Serializing/Reader.cs
@@ -266,6 +266,20 @@ internal byte PeekByte()
{
return _buffer[Position];
}
+
+ ///
+ /// Returns the next uint16 to be read.
+ ///
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal ushort PeekUInt16()
+ {
+ ushort result = 0;
+ result |= _buffer[Position];
+ result |= (ushort)(_buffer[Position+1] << 8);
+
+ return result;
+ }
///
/// Skips a number of bytes in the reader.
diff --git a/Assets/FishNet/Runtime/Transporting/EventStructures.cs b/Assets/FishNet/Runtime/Transporting/EventStructures.cs
index bbbfc83ed..cab273f0f 100644
--- a/Assets/FishNet/Runtime/Transporting/EventStructures.cs
+++ b/Assets/FishNet/Runtime/Transporting/EventStructures.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using FishNet.Object;
namespace FishNet.Transporting
{
@@ -95,6 +96,28 @@ public ClientReceivedDataArgs(ArraySegment data, Channel channel, int tran
TransportIndex = transportIndex;
}
}
+
+ ///
+ /// Container about data received on the local client.
+ ///
+ public struct PacketProcessingArgs
+ {
+ public NetworkBehaviour NetworkBehaviour;
+
+ public PacketId PacketId;
+
+ public int DataLength;
+
+ public int PropertyHash;
+
+ public PacketProcessingArgs(NetworkBehaviour nb, int propertyHash, PacketId packetId, int dataLength)
+ {
+ NetworkBehaviour = nb;
+ PacketId = packetId;
+ DataLength = dataLength;
+ PropertyHash = propertyHash;
+ }
+ }
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling.meta b/Assets/FishNet/Runtime/Utility/Performance/Profiling.meta
new file mode 100644
index 000000000..ca902e0fb
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 084aef25b51176a43978f1cfe92272db
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/CountRecorder.cs b/Assets/FishNet/Runtime/Utility/Performance/Profiling/CountRecorder.cs
new file mode 100644
index 000000000..2e3954e45
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/CountRecorder.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+using FishNet.Transporting;
+using Unity.Profiling;
+using UnityEngine;
+
+namespace FishNet.Utility.Performance.Profiling
+{
+ internal class CountRecorder
+ {
+ private readonly ProfilerCounter _profilerCount;
+ private readonly ProfilerCounter _profilerBytes;
+ private readonly ProfilerCounter _profilerPerSecond;
+ private readonly object _instance;
+ //private readonly INetworkInfoProvider _provider;
+ internal readonly Frames _frames;
+ private int _count;
+ private int _bytes;
+ private int _perSecond;
+ private readonly Queue<(float time, int bytes)> _perSecondQueue = new Queue<(float time, int bytes)>();
+ private int _frameIndex = -1;
+
+ public CountRecorder(object instance, /*INetworkInfoProvider provider,*/ ProfilerCounter profilerCount, ProfilerCounter profilerBytes, ProfilerCounter profilerPerSecond)
+ {
+ //_provider = provider;
+ _instance = instance;
+ _profilerCount = profilerCount;
+ _profilerBytes = profilerBytes;
+ _profilerPerSecond = profilerPerSecond;
+ _frames = new Frames();
+ }
+
+ public void OnMessage(PacketProcessingArgs args)
+ {
+
+ // dont record anything if frame index is -1
+ // this normally means the profiler has not been activated yet
+ if (_frameIndex == -1)
+ return;
+
+ _count ++;
+ _bytes += args.DataLength;
+
+ var frame = _frames.GetFrame(_frameIndex);
+ frame.Messages.Add(
+ new PacketInfo(
+ args,
+ frame.Messages.Count));
+ frame.Bytes+= args.DataLength;
+ }
+
+ public void EndFrame(int frameIndex)
+ {
+ CaclulatePerSecond(Time.time, _bytes);
+ _profilerCount.Sample(_count);
+ _profilerBytes.Sample(_bytes);
+ _count = 0;
+ _bytes = 0;
+
+ // +1 so that we set next frame
+ // otherwise we clear the frame that the savedata wants to grab
+ _frameIndex = frameIndex + 1;
+ var frame = _frames.GetFrame(_frameIndex);
+ frame.Messages.Clear();
+ frame.Bytes = 0;
+ }
+
+ private void CaclulatePerSecond(float now, int bytes)
+ {
+ // add new values to sum
+ _perSecond += bytes;
+ _perSecondQueue.Enqueue((now, bytes));
+
+ // remove old bytes from sum
+ var removeTime = now - 1;
+ while (_perSecondQueue.Peek().time < removeTime)
+ {
+ var removed = _perSecondQueue.Dequeue();
+ _perSecond -= removed.bytes;
+ }
+
+ // record sample after adding/removing value
+ _profilerPerSecond.Sample(_perSecond);
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/CountRecorder.cs.meta b/Assets/FishNet/Runtime/Utility/Performance/Profiling/CountRecorder.cs.meta
new file mode 100644
index 000000000..dd5874983
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/CountRecorder.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 16575ee3be2c5e046af4438787d215f0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/Counters.cs b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Counters.cs
new file mode 100644
index 000000000..aecffbd05
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Counters.cs
@@ -0,0 +1,25 @@
+using Fishnet.NetworkProfiler;
+using Unity.Profiling;
+
+namespace FishNet.Utility.Performance.Profiling
+{
+ internal static class Counters
+ {
+ public static readonly ProfilerCategory Category = ProfilerCategory.Network;
+ private const ProfilerMarkerDataUnit COUNT = ProfilerMarkerDataUnit.Count;
+ private const ProfilerMarkerDataUnit BYTES = ProfilerMarkerDataUnit.Bytes;
+
+ public static readonly ProfilerCounter AllPlayersCount = new ProfilerCounter(Category, Names.PLAYER_COUNT, COUNT);
+ public static readonly ProfilerCounter AuthenticatedPlayersCount = new ProfilerCounter(Category, Names.AUTHENTICATED_COUNT, COUNT);
+ public static readonly ProfilerCounter CharacterCount = new ProfilerCounter(Category, Names.CHARACTER_COUNT, COUNT);
+ public static readonly ProfilerCounter ObjectCount = new ProfilerCounter(Category, Names.OBJECT_COUNT, COUNT);
+
+ public static readonly ProfilerCounter SentCount = new ProfilerCounter(Category, Names.SENT_COUNT, COUNT);
+ public static readonly ProfilerCounter SentBytes = new ProfilerCounter(Category, Names.SENT_BYTES, BYTES);
+ public static readonly ProfilerCounter SentPerSecond = new ProfilerCounter(Category, Names.SENT_PER_SECOND, BYTES);
+
+ public static readonly ProfilerCounter ReceiveCount = new ProfilerCounter(Category, Names.RECEIVED_COUNT, COUNT);
+ public static readonly ProfilerCounter ReceiveBytes = new ProfilerCounter(Category, Names.RECEIVED_BYTES, BYTES);
+ public static readonly ProfilerCounter ReceivePerSecond = new ProfilerCounter(Category, Names.RECEIVED_PER_SECOND, BYTES);
+ }
+}
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/Counters.cs.meta b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Counters.cs.meta
new file mode 100644
index 000000000..325bf4506
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Counters.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 40d8d596129601f4d90f0664cb2557c4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/Frame.cs b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Frame.cs
new file mode 100644
index 000000000..da0be4c95
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Frame.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace FishNet.Utility.Performance.Profiling
+{
+ [System.Serializable]
+ public class Frames : IEnumerable
+ {
+ [SerializeField]
+ private Frame[] _frames;
+
+ public Frames()
+ {
+ _frames = new Frame[NetworkProfilerRecorder.FRAME_COUNT];
+ for (var i = 0; i < _frames.Length; i++)
+ _frames[i] = new Frame();
+ }
+
+ public Frame GetFrame(int frameIndex)
+ {
+ return _frames[frameIndex % _frames.Length];
+ }
+
+ public IEnumerator GetEnumerator() => ((IEnumerable)_frames).GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_frames).GetEnumerator();
+
+ internal void ValidateSize()
+ {
+ if (_frames.Length != NetworkProfilerRecorder.FRAME_COUNT)
+ {
+ Array.Resize(ref _frames, NetworkProfilerRecorder.FRAME_COUNT);
+ }
+ }
+ }
+
+ [System.Serializable]
+ public class Frame
+ {
+ public List Messages = new List();
+ public int Bytes;
+ }
+}
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/Frame.cs.meta b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Frame.cs.meta
new file mode 100644
index 000000000..cbe424201
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Frame.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b59754fd175ff2345a6788891dddbfcf
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/Names.cs b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Names.cs
new file mode 100644
index 000000000..6f849d910
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Names.cs
@@ -0,0 +1,25 @@
+namespace FishNet.Utility.Performance.Profiling
+{
+ public static class Names
+ {
+ public const string PLAYER_COUNT = "All Players Count";
+ public const string PLAYER_COUNT_TOOLTIP = "Number of players connected to the server";
+ public const string AUTHENTICATED_COUNT = "Authenticated Players Count";
+ public const string AUTHENTICATED_COUNT_TOOLTIP = "Number of Authenticated players connected to the server, This should be same as total players most of the time";
+ public const string CHARACTER_COUNT = "Character Count";
+ public const string CHARACTER_COUNT_TOOLTIP = "Number of players with spawned GameObjects";
+
+ public const string OBJECT_COUNT = "Object Count";
+ public const string OBJECT_COUNT_TOOLTIP = "Number of NetworkObjects spawned";
+
+ public const string SENT_COUNT = "Sent Messages";
+ public const string SENT_BYTES = "Sent Bytes";
+ public const string SENT_PER_SECOND = "Sent Per Second";
+
+ public const string RECEIVED_COUNT = "Received Messages";
+ public const string RECEIVED_BYTES = "Received Bytes";
+ public const string RECEIVED_PER_SECOND = "Received Per Second";
+
+ public const string PER_SECOND_TOOLTIP = "Sum of Bytes over the previous second";
+ }
+}
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/Names.cs.meta b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Names.cs.meta
new file mode 100644
index 000000000..3e12e8598
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/Names.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 05fd6c7133d9b2e42a8f282465a445d8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/NetworkProfilerRecorder.cs b/Assets/FishNet/Runtime/Utility/Performance/Profiling/NetworkProfilerRecorder.cs
new file mode 100644
index 000000000..7e4768b1b
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/NetworkProfilerRecorder.cs
@@ -0,0 +1,181 @@
+using UnityEngine;
+using System.Linq;
+using FishNet.Managing.Client;
+using FishNet.Managing.Server;
+using FishNet.Transporting;
+
+
+#if UNITY_EDITOR
+using UnityEditorInternal;
+#endif
+
+namespace FishNet.Utility.Performance.Profiling
+{
+ [DefaultExecutionOrder(int.MaxValue)] // last
+ public class NetworkProfilerRecorder : MonoBehaviour
+ {
+ // singleton because unity only has 1 profiler
+ public static NetworkProfilerRecorder Instance { get; private set; }
+
+ public ClientManager Client;
+ public ServerManager Server;
+
+ internal static CountRecorder _sentCounter;
+ internal static CountRecorder _receivedCounter;
+ internal const int FRAME_COUNT = 300; // todo find a way to get real frame count
+
+ public delegate void FrameUpdate(int tick);
+ public static event FrameUpdate AfterSample;
+
+ private int _lastProcessedFrame = -1;
+
+ private void Start()
+ {
+#if UNITY_EDITOR
+ _lastProcessedFrame = ProfilerDriver.lastFrameIndex;
+#endif
+
+ Debug.Assert(Instance == null);
+ Instance = this;
+ DontDestroyOnLoad(this);
+
+ // if (Server != null)
+ // {
+ // Server.;
+ // Server.Stopped.AddListener(ServerStopped);
+ // }
+ if (Client == null)
+ {
+ Client = InstanceFinder.ClientManager;
+ }
+
+ if (Client != null)
+ {
+ Client.OnClientConnectionState += ClientConnectionStateChanged;
+ }
+ }
+
+ private void OnDestroy()
+ {
+ //if (_receivedCounter != null)
+ // NetworkDiagnostics.InMessageEvent -= _receivedCounter.OnMessage;
+ //if (_sentCounter != null)
+ // NetworkDiagnostics.OutMessageEvent -= _sentCounter.OnMessage;
+
+ Debug.Assert(Instance == this);
+ Instance = null;
+ }
+
+ private void ServerStarted()
+ {
+ //if (instance != null)
+ //{
+ // logger.LogWarning($"Already started profiler for different Instance:{instance}");
+ // return;
+ //}
+ //instance = Server;
+
+ //var provider = new NetworkInfoProvider(Server.World);
+ //_sentCounter = new CountRecorder(Server, provider, Counters.SentCount, Counters.SentBytes, Counters.SentPerSecond);
+ //_receivedCounter = new CountRecorder(Server, provider, Counters.ReceiveCount, Counters.ReceiveBytes, Counters.ReceivePerSecond);
+ //NetworkDiagnostics.InMessageEvent += _receivedCounter.OnMessage;
+ //NetworkDiagnostics.OutMessageEvent += _sentCounter.OnMessage;
+ }
+
+ private void ClientConnectionStateChanged(ClientConnectionStateArgs args)
+ {
+ switch (args.ConnectionState)
+ {
+ case LocalConnectionState.Started:
+ ClientStarted();
+ break;
+ case LocalConnectionState.Stopped:
+ ClientStopped();
+ break;
+ }
+ }
+
+ private void ClientStarted()
+ {
+ _sentCounter = new CountRecorder(Client, Counters.ReceiveCount, Counters.ReceiveBytes, Counters.ReceivePerSecond);
+ _receivedCounter = new CountRecorder(Client, Counters.ReceiveCount, Counters.ReceiveBytes, Counters.ReceivePerSecond);
+ Client.OnPacketRead += _receivedCounter.OnMessage;
+ //NetworkDiagnostics.OutMessageEvent += _sentCounter.OnMessage;
+ }
+
+ private void ServerStopped()
+ {
+ //if (instance == (object)Server)
+ // instance = null;
+ }
+
+ private void ClientStopped(/*ClientStoppedReason _*/)
+ {
+ _receivedCounter = null;
+ _sentCounter = null;
+ }
+
+ private void LateUpdate()
+ {
+#if UNITY_EDITOR
+ if (!ProfilerDriver.enabled)
+ return;
+
+ // once a frame, ever frame, no matter what lastFrameIndex is
+ SampleCounts();
+
+ // unity sometimes skips a profiler frame, because unity
+ // so we have to check if that happens and then sample the missing frame
+ while (_lastProcessedFrame < ProfilerDriver.lastFrameIndex)
+ {
+ _lastProcessedFrame++;
+
+ //Debug.Log($"Sample: [LateUpdate, enabled { ProfilerDriver.enabled}, first {ProfilerDriver.firstFrameIndex}, last {ProfilerDriver.lastFrameIndex}, lastProcessed {lastProcessedFrame}]");
+
+ var lastFrame = _lastProcessedFrame;
+ // not sure why frame is offset, but +2 fixes it
+ SampleMessages(lastFrame + 2);
+ }
+#else
+ // in player, just use ProfilerCounter (frameCount only used by messages)
+ SampleCounts();
+ SampleMessages(0);
+#endif
+ }
+
+ ///
+ /// call this every frame to sample number of players and objects
+ ///
+ private void SampleCounts()
+ {
+ if (Client != null)
+ {
+ Counters.AllPlayersCount.Sample(Client.Clients.Count);
+ Counters.ObjectCount.Sample(Client.Objects.Spawned.Count);
+ } else if (Server != null)
+ {
+ Counters.AllPlayersCount.Sample(Server.Clients.Count);
+ Counters.ObjectCount.Sample(Server.Objects.Spawned.Count);
+ }
+ }
+
+ ///
+ /// call this when ProfilerDriver shows it is next frame
+ ///
+ ///
+ private void SampleMessages(int frame)
+ {
+
+ if (_sentCounter != null)
+ {
+ _sentCounter.EndFrame(frame);
+ }
+
+ if (_receivedCounter != null)
+ {
+ _receivedCounter.EndFrame(frame);
+ }
+ AfterSample?.Invoke(frame);
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/NetworkProfilerRecorder.cs.meta b/Assets/FishNet/Runtime/Utility/Performance/Profiling/NetworkProfilerRecorder.cs.meta
new file mode 100644
index 000000000..83dd31721
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/NetworkProfilerRecorder.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bc5b42cbfc38b1c4d86fd25905b19e29
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfo.cs b/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfo.cs
new file mode 100644
index 000000000..ec95a08a7
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfo.cs
@@ -0,0 +1,42 @@
+using FishNet.Transporting;
+using UnityEngine;
+
+namespace FishNet.Utility.Performance.Profiling
+{
+ [System.Serializable]
+ public class PacketInfo
+ {
+ ///
+ /// Order message was sent/received in frame
+ ///
+ [SerializeField] private int _order;
+ [SerializeField] private int _length;
+ [SerializeField] private int _count;
+ [SerializeField] private int _packetId;
+ [SerializeField] private string _objectName;
+ [SerializeField] private string _rpcName;
+
+ public int Order => _order;
+ public int Bytes => _length;
+ public int Count => _count;
+ public int TotalBytes => Bytes * Count;
+ public string ObjectName => _objectName;
+ public string PacketIdName => ((PacketId)_packetId).ToString();
+ public string RpcName => _rpcName;
+
+ public PacketInfo(PacketProcessingArgs args, int order)
+ {
+ _order = order;
+ _length = args.DataLength;
+ _count = 1;
+ _packetId = (int)args.PacketId;
+ if (args.NetworkBehaviour != null)
+ {
+ _objectName = args.NetworkBehaviour.name;
+ // var obj = provider.GetNetworkIdentity(id);
+ // _objectName = obj != null ? obj.name : null;
+ _rpcName = PacketInfoProvider.GetPropertyName(args.NetworkBehaviour, args.PropertyHash, args.PacketId);
+ }
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfo.cs.meta b/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfo.cs.meta
new file mode 100644
index 000000000..2d173f0d9
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfo.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 668dc7c434784214794807fcfecfe185
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfoProvider.cs b/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfoProvider.cs
new file mode 100644
index 000000000..95d4820a8
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfoProvider.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using FishNet.Object;
+using FishNet.Object.Prediction;
+using FishNet.Object.Synchronizing;
+using FishNet.Transporting;
+
+namespace FishNet.Utility.Performance.Profiling
+{
+
+ public static class PacketInfoProvider
+ {
+ private static Dictionary> SyncTypeNamesCache = new();
+ private static Dictionary> RpcNamesCache = new();
+
+ public static string GetPropertyName(NetworkBehaviour nb, int propertyHash, PacketId packetId)
+ {
+ string res = "";
+ switch (packetId)
+ {
+ case PacketId.SyncObject:
+ case PacketId.SyncVar:
+ if (SyncTypeNamesCache.TryGetValue(nb.GetType(), out var syncTypeNames))
+ {
+ if (propertyHash < syncTypeNames.Count)
+ {
+ res = syncTypeNames[propertyHash];
+ }
+ }
+ else
+ {
+ List typeNamesList = new();
+ int syncTypeCount = 0;
+ foreach (var fieldInfo in nb.GetType().GetFields())
+ {
+ foreach (var customAttribute in fieldInfo.CustomAttributes)
+ {
+ if (customAttribute.AttributeType.FullName == typeof(SyncVarAttribute).FullName ||
+ customAttribute.AttributeType.FullName == typeof(SyncObjectAttribute).FullName)
+ {
+ if (syncTypeCount == propertyHash)
+ {
+ res = fieldInfo.Name;
+ }
+ syncTypeCount++;
+ typeNamesList.Add(fieldInfo.Name);
+ }
+ }
+ }
+
+ SyncTypeNamesCache[nb.GetType()] = typeNamesList;
+ }
+ break;
+ case PacketId.Reconcile:
+ case PacketId.ObserversRpc:
+ case PacketId.TargetRpc:
+ if (RpcNamesCache.TryGetValue(nb.GetType(), out var rpcNames))
+ {
+ if (propertyHash < rpcNames.Count)
+ {
+ res = rpcNames[propertyHash];
+ }
+ }
+ else
+ {
+ List rpcNamesList = new();
+ int rpcCount = 0;
+ // Iterate the replicates first, that's what the codegen does
+ foreach (var methodInfo in nb.GetType().GetMethods())
+ {
+ foreach (var customAttribute in methodInfo.CustomAttributes)
+ {
+ if (customAttribute.AttributeType.FullName == typeof(ReplicateAttribute).FullName)
+ {
+ rpcNamesList.Add(methodInfo.Name);
+ rpcCount++;
+ }
+ }
+ }
+ foreach (var methodInfo in nb.GetType().GetMethods())
+ {
+ foreach (var customAttribute in methodInfo.CustomAttributes)
+ {
+ if (customAttribute.AttributeType.FullName == typeof(ObserversRpcAttribute).FullName ||
+ customAttribute.AttributeType.FullName == typeof(TargetRpcAttribute).FullName ||
+ customAttribute.AttributeType.FullName == typeof(ReconcileAttribute).FullName)
+ {
+ if (rpcCount == propertyHash)
+ {
+ res = methodInfo.Name;
+ break;
+ }
+ rpcCount++;
+ rpcNamesList.Add(methodInfo.Name);
+ }
+ }
+ }
+
+ RpcNamesCache[nb.GetType()] = rpcNamesList;
+ }
+ break;
+ }
+
+ return res;
+ }
+ }
+}
diff --git a/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfoProvider.cs.meta b/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfoProvider.cs.meta
new file mode 100644
index 000000000..ea174bf95
--- /dev/null
+++ b/Assets/FishNet/Runtime/Utility/Performance/Profiling/PacketInfoProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8192a6dbec7e3b446bea79c4b0f172be
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/FishNet/VERSION.txt b/Assets/FishNet/VERSION.txt
index 03d4bc90f..4dcb6e64e 100644
--- a/Assets/FishNet/VERSION.txt
+++ b/Assets/FishNet/VERSION.txt
@@ -1 +1 @@
-3.11.18
\ No newline at end of file
+3.11.19
\ No newline at end of file
diff --git a/Assets/FishNet/package.json b/Assets/FishNet/package.json
index 1c9baf12c..830b13cb2 100644
--- a/Assets/FishNet/package.json
+++ b/Assets/FishNet/package.json
@@ -1,6 +1,6 @@
{
"name": "com.firstgeargames.fishnet",
- "version": "3.11.18",
+ "version": "3.11.19",
"displayName": "FishNet: Networking Evolved",
"description": "A feature-rich Unity networking solution aimed towards reliability, ease of use, efficiency, and flexibility.",
"unity": "2021.3",
@@ -25,6 +25,7 @@
"url": "http://www.firstgeargames.com"
},
"dependencies": {
- "com.unity.nuget.newtonsoft-json": "3.2.1"
+ "com.unity.nuget.newtonsoft-json": "3.2.1",
+ "com.unity.profiling.core": "1.0.2"
}
}
\ No newline at end of file
diff --git a/FishNet.sln b/FishNet.sln
index 89338b6ce..7813bf63c 100644
--- a/FishNet.sln
+++ b/FishNet.sln
@@ -1,60 +1,143 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishNet.Runtime", "FishNet.Runtime.csproj", "{69D0C159-BAB4-689B-687E-9E213DFB5A44}"
+Microsoft Visual Studio Solution File, Format Version 11.00
+# Visual Studio 2010
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishNet.Runtime", "FishNet.Runtime.csproj", "{59c1d069-b4ba-9b68-687e-9e213dfb5a44}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameKit.Utilities", "GameKit.Utilities.csproj", "{13113C11-58E9-2A2E-DF1A-460F73B189CE}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityEditor.UI", "UnityEditor.UI.csproj", "{6ff9d838-069f-4b49-5a35-f70072bbf0bf}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishNet.Demos", "FishNet.Demos.csproj", "{136A7A12-C446-BC06-2B59-E13B4B55C391}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishNet.Demos", "FishNet.Demos.csproj", "{127a6a13-46c4-06bc-2b59-e13b4b55c391}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp", "Assembly-CSharp.csproj", "{5ED35FB8-7A21-7C15-DB72-386947C92520}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.TestTools.CodeCoverage.Editor", "Unity.TestTools.CodeCoverage.Editor.csproj", "{2fefaaf8-b5d9-71e6-28d0-ea11b956c5ab}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameKit.Dependencies", "GameKit.Dependencies.csproj", "{6D29C1FC-6376-8F6B-216E-88C8C2471B53}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishNet.Codegen.Cecil", "FishNet.Codegen.Cecil.csproj", "{537888d1-cf8a-a5f4-08f5-be1b0604d38e}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.FishNet.Codegen", "Unity.FishNet.Codegen.csproj", "{486584D8-1B68-74D0-B2EE-6A18C8D7352A}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityEditor.TestRunner", "UnityEditor.TestRunner.csproj", "{75207961-aee3-5a76-7646-e1380f658e8a}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "asset-store-tools-editor", "asset-store-tools-editor.csproj", "{B5A117C4-B458-5028-BA98-4F8E4154A101}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityEngine.TestRunner", "UnityEngine.TestRunner.csproj", "{9cb64bdf-5b52-b4fa-5298-d7fd83a65e4b}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FishNet.Codegen.Cecil", "FishNet.Codegen.Cecil.csproj", "{D1887853-8ACF-F4A5-08F5-BE1B0604D38E}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityEngine.UI", "UnityEngine.UI.csproj", "{9aa0345b-376b-0323-106e-cc6cb6d71a2d}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Rider.Editor", "Unity.Rider.Editor.csproj", "{a319fe15-1ae6-fe2a-0a9f-052921a7f1a8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Edgegap", "Edgegap.csproj", "{1ad87cc8-ed38-0ca8-9bc0-9e4d761874cc}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.FishNet.Codegen", "Unity.FishNet.Codegen.csproj", "{d8846548-681b-d074-b2ee-6a18c8d7352a}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.VisualStudio.Editor", "Unity.VisualStudio.Editor.csproj", "{531f45d2-6b02-0890-bdab-1a1cfec120fe}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Settings.Editor", "Unity.Settings.Editor.csproj", "{3c2e65d1-71a9-dcce-9ff0-e462821b8936}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.TestTools.CodeCoverage.Editor.OpenCover.Model", "Unity.TestTools.CodeCoverage.Editor.OpenCover.Model.csproj", "{cb069737-b94e-9a1c-359c-e91e2cc14ccf}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.SysrootPackage.Editor", "Unity.SysrootPackage.Editor.csproj", "{d1b151e9-55af-d0be-5c48-e60322e8547e}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Profiling.Core", "Unity.Profiling.Core.csproj", "{11c38925-f5ac-1a7b-d4ea-7d1dbe8333b9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameKit.Utilities", "GameKit.Utilities.csproj", "{113c1113-e958-2e2a-df1a-460f73b189ce}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Performance.Profile-Analyzer.Editor", "Unity.Performance.Profile-Analyzer.Editor.csproj", "{892dd2ad-d637-2009-2a24-a9e22d565f04}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.VSCode.Editor", "Unity.VSCode.Editor.csproj", "{054be889-cbbb-67ac-ee1c-c274bed62ef9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.EditorCoroutines.Editor", "Unity.EditorCoroutines.Editor.csproj", "{2f4a3448-cdaf-4526-5f2b-e6251050fa57}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameKit.Dependencies", "GameKit.Dependencies.csproj", "{fcc1296d-7663-6b8f-216e-88c8c2471b53}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.TestTools.CodeCoverage.Editor.OpenCover.Mono.Reflection", "Unity.TestTools.CodeCoverage.Editor.OpenCover.Mono.Reflection.csproj", "{e9f11526-f647-3906-4076-b9d63956a2f2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Sysroot.Linux_x86_64", "Unity.Sysroot.Linux_x86_64.csproj", "{e3bb3ef4-4fde-ed23-c68f-bde7736f97e7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Toolchain.Win-x86_64-Linux-x86_64", "Unity.Toolchain.Win-x86_64-Linux-x86_64.csproj", "{dcbe98c1-58fc-f809-893a-f5d2f6943897}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp", "Assembly-CSharp.csproj", "{b85fd35e-217a-157c-db72-386947c92520}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityEngine.UI.Tests", "UnityEngine.UI.Tests.csproj", "{67278720-21fb-059d-f163-359f038172c3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityEditor.UI.EditorTests", "UnityEditor.UI.EditorTests.csproj", "{ce465a3d-fd6c-d3ce-4d58-f60566f39213}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Toolchain.Win-x86_64-Linux-x86_64.EditorTests", "Unity.Toolchain.Win-x86_64-Linux-x86_64.EditorTests.csproj", "{96d65cec-822f-7bc1-485f-2689af7c678a}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Sysroot.Linux_x86_64.EditorTests", "Unity.Sysroot.Linux_x86_64.EditorTests.csproj", "{24a55edd-b57b-5260-7140-c808091ae8c0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.TestTools.CodeCoverage.Editor.Compatibility", "Unity.TestTools.CodeCoverage.Editor.Compatibility.csproj", "{d4aff916-632e-ee01-58e3-a5fe1c465dbf}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Settings.Tests", "Unity.Settings.Tests.csproj", "{bf826988-d794-8d15-17c3-30e002b58d3c}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Performance.Profile-Analyzer.EditorTests", "Unity.Performance.Profile-Analyzer.EditorTests.csproj", "{cb503895-a385-b47b-b7f5-142e6fa90c8e}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unity.Sysroot.EditorTests", "Unity.Sysroot.EditorTests.csproj", "{669fd055-17cb-6645-809a-9f6916f3ff2e}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {69D0C159-BAB4-689B-687E-9E213DFB5A44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {69D0C159-BAB4-689B-687E-9E213DFB5A44}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {69D0C159-BAB4-689B-687E-9E213DFB5A44}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {69D0C159-BAB4-689B-687E-9E213DFB5A44}.Release|Any CPU.Build.0 = Release|Any CPU
- {13113C11-58E9-2A2E-DF1A-460F73B189CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {13113C11-58E9-2A2E-DF1A-460F73B189CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {13113C11-58E9-2A2E-DF1A-460F73B189CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {13113C11-58E9-2A2E-DF1A-460F73B189CE}.Release|Any CPU.Build.0 = Release|Any CPU
- {136A7A12-C446-BC06-2B59-E13B4B55C391}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {136A7A12-C446-BC06-2B59-E13B4B55C391}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {136A7A12-C446-BC06-2B59-E13B4B55C391}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {136A7A12-C446-BC06-2B59-E13B4B55C391}.Release|Any CPU.Build.0 = Release|Any CPU
- {5ED35FB8-7A21-7C15-DB72-386947C92520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5ED35FB8-7A21-7C15-DB72-386947C92520}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5ED35FB8-7A21-7C15-DB72-386947C92520}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5ED35FB8-7A21-7C15-DB72-386947C92520}.Release|Any CPU.Build.0 = Release|Any CPU
- {6D29C1FC-6376-8F6B-216E-88C8C2471B53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6D29C1FC-6376-8F6B-216E-88C8C2471B53}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6D29C1FC-6376-8F6B-216E-88C8C2471B53}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6D29C1FC-6376-8F6B-216E-88C8C2471B53}.Release|Any CPU.Build.0 = Release|Any CPU
- {486584D8-1B68-74D0-B2EE-6A18C8D7352A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {486584D8-1B68-74D0-B2EE-6A18C8D7352A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {486584D8-1B68-74D0-B2EE-6A18C8D7352A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {486584D8-1B68-74D0-B2EE-6A18C8D7352A}.Release|Any CPU.Build.0 = Release|Any CPU
- {B5A117C4-B458-5028-BA98-4F8E4154A101}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5A117C4-B458-5028-BA98-4F8E4154A101}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B5A117C4-B458-5028-BA98-4F8E4154A101}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B5A117C4-B458-5028-BA98-4F8E4154A101}.Release|Any CPU.Build.0 = Release|Any CPU
- {D1887853-8ACF-F4A5-08F5-BE1B0604D38E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D1887853-8ACF-F4A5-08F5-BE1B0604D38E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D1887853-8ACF-F4A5-08F5-BE1B0604D38E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D1887853-8ACF-F4A5-08F5-BE1B0604D38E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {59c1d069-b4ba-9b68-687e-9e213dfb5a44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {59c1d069-b4ba-9b68-687e-9e213dfb5a44}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6ff9d838-069f-4b49-5a35-f70072bbf0bf}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6ff9d838-069f-4b49-5a35-f70072bbf0bf}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {127a6a13-46c4-06bc-2b59-e13b4b55c391}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {127a6a13-46c4-06bc-2b59-e13b4b55c391}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2fefaaf8-b5d9-71e6-28d0-ea11b956c5ab}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2fefaaf8-b5d9-71e6-28d0-ea11b956c5ab}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {537888d1-cf8a-a5f4-08f5-be1b0604d38e}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {537888d1-cf8a-a5f4-08f5-be1b0604d38e}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {75207961-aee3-5a76-7646-e1380f658e8a}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {75207961-aee3-5a76-7646-e1380f658e8a}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9cb64bdf-5b52-b4fa-5298-d7fd83a65e4b}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9cb64bdf-5b52-b4fa-5298-d7fd83a65e4b}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9aa0345b-376b-0323-106e-cc6cb6d71a2d}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9aa0345b-376b-0323-106e-cc6cb6d71a2d}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {a319fe15-1ae6-fe2a-0a9f-052921a7f1a8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {a319fe15-1ae6-fe2a-0a9f-052921a7f1a8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1ad87cc8-ed38-0ca8-9bc0-9e4d761874cc}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1ad87cc8-ed38-0ca8-9bc0-9e4d761874cc}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {d8846548-681b-d074-b2ee-6a18c8d7352a}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {d8846548-681b-d074-b2ee-6a18c8d7352a}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {531f45d2-6b02-0890-bdab-1a1cfec120fe}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {531f45d2-6b02-0890-bdab-1a1cfec120fe}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3c2e65d1-71a9-dcce-9ff0-e462821b8936}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3c2e65d1-71a9-dcce-9ff0-e462821b8936}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {cb069737-b94e-9a1c-359c-e91e2cc14ccf}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {cb069737-b94e-9a1c-359c-e91e2cc14ccf}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {d1b151e9-55af-d0be-5c48-e60322e8547e}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {d1b151e9-55af-d0be-5c48-e60322e8547e}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {11c38925-f5ac-1a7b-d4ea-7d1dbe8333b9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {11c38925-f5ac-1a7b-d4ea-7d1dbe8333b9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {113c1113-e958-2e2a-df1a-460f73b189ce}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {113c1113-e958-2e2a-df1a-460f73b189ce}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {892dd2ad-d637-2009-2a24-a9e22d565f04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {892dd2ad-d637-2009-2a24-a9e22d565f04}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {054be889-cbbb-67ac-ee1c-c274bed62ef9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {054be889-cbbb-67ac-ee1c-c274bed62ef9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2f4a3448-cdaf-4526-5f2b-e6251050fa57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2f4a3448-cdaf-4526-5f2b-e6251050fa57}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {fcc1296d-7663-6b8f-216e-88c8c2471b53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {fcc1296d-7663-6b8f-216e-88c8c2471b53}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {e9f11526-f647-3906-4076-b9d63956a2f2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {e9f11526-f647-3906-4076-b9d63956a2f2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {e3bb3ef4-4fde-ed23-c68f-bde7736f97e7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {e3bb3ef4-4fde-ed23-c68f-bde7736f97e7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {dcbe98c1-58fc-f809-893a-f5d2f6943897}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {dcbe98c1-58fc-f809-893a-f5d2f6943897}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {b85fd35e-217a-157c-db72-386947c92520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {b85fd35e-217a-157c-db72-386947c92520}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {67278720-21fb-059d-f163-359f038172c3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {67278720-21fb-059d-f163-359f038172c3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ce465a3d-fd6c-d3ce-4d58-f60566f39213}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ce465a3d-fd6c-d3ce-4d58-f60566f39213}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {96d65cec-822f-7bc1-485f-2689af7c678a}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {96d65cec-822f-7bc1-485f-2689af7c678a}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {24a55edd-b57b-5260-7140-c808091ae8c0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {24a55edd-b57b-5260-7140-c808091ae8c0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {d4aff916-632e-ee01-58e3-a5fe1c465dbf}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {d4aff916-632e-ee01-58e3-a5fe1c465dbf}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {bf826988-d794-8d15-17c3-30e002b58d3c}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {bf826988-d794-8d15-17c3-30e002b58d3c}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {cb503895-a385-b47b-b7f5-142e6fa90c8e}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {cb503895-a385-b47b-b7f5-142e6fa90c8e}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {669fd055-17cb-6645-809a-9f6916f3ff2e}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {669fd055-17cb-6645-809a-9f6916f3ff2e}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/package.json b/package.json
deleted file mode 100644
index 456c627f1..000000000
--- a/package.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "name": "com.firstgeargames.fishnet",
- "version": "3.11.15",
- "displayName": "FishNet: Networking Evolved",
- "description": "A feature-rich Unity networking solution aimed towards reliability, ease of use, efficiency, and flexibility.",
- "unity": "2019.4",
- "documentationUrl": "https://fish-networking.gitbook.io/docs/",
- "licensesUrl": "https://github.com/FirstGearGames/FishNet/blob/main/LICENSE.md",
- "license": "See LICENSE.txt file",
- "keywords": [
- "FishNetworking",
- "Networking",
- "FishNet",
- "Unity Networking",
- "Bolt",
- "Mirror",
- "Photon",
- "DarkRift",
- "DedicatedServer",
- "Fusion",
- "Netcode"
- ],
- "author": {
- "name": "FirstGearGames",
- "url": "http://www.firstgeargames.com"
- }
-}
\ No newline at end of file
diff --git a/packages.config b/packages.config
deleted file mode 100644
index d6a8f0776..000000000
--- a/packages.config
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file