diff --git a/Assets/Mirage/Runtime/ClientObjectManager.cs b/Assets/Mirage/Runtime/ClientObjectManager.cs index 80316164c6..5e5430bd65 100644 --- a/Assets/Mirage/Runtime/ClientObjectManager.cs +++ b/Assets/Mirage/Runtime/ClientObjectManager.cs @@ -708,9 +708,7 @@ internal void OnRpcMessage(RpcMessage msg) return; } - var behaviour = identity.NetworkBehaviours[msg.ComponentIndex]; - - var remoteCall = behaviour.RemoteCallCollection.Get(msg.FunctionIndex); + var remoteCall = identity.RemoteCallCollection.GetAbsolute(msg.FunctionIndex); if (remoteCall.InvokeType != RpcInvokeType.ClientRpc) { @@ -719,7 +717,7 @@ internal void OnRpcMessage(RpcMessage msg) using (var reader = NetworkReaderPool.GetReader(msg.Payload, _client.World)) { - remoteCall.Invoke(reader, behaviour, null, 0); + remoteCall.Invoke(reader, null, 0); } } diff --git a/Assets/Mirage/Runtime/Messages.cs b/Assets/Mirage/Runtime/Messages.cs index 0096f746ea..8697a9471a 100644 --- a/Assets/Mirage/Runtime/Messages.cs +++ b/Assets/Mirage/Runtime/Messages.cs @@ -39,7 +39,6 @@ public struct SceneReadyMessage { } public struct ServerRpcMessage { public uint NetId; - public int ComponentIndex; public int FunctionIndex; // the parameters for the Cmd function @@ -51,7 +50,6 @@ public struct ServerRpcMessage public struct ServerRpcWithReplyMessage { public uint NetId; - public int ComponentIndex; public int FunctionIndex; // if the server Rpc can return values @@ -72,7 +70,6 @@ public struct ServerRpcReply public struct RpcMessage { public uint NetId; - public int ComponentIndex; public int FunctionIndex; public ArraySegment Payload; } diff --git a/Assets/Mirage/Runtime/NetworkBehaviour.cs b/Assets/Mirage/Runtime/NetworkBehaviour.cs index 1013bbbdf4..55b1de47b1 100644 --- a/Assets/Mirage/Runtime/NetworkBehaviour.cs +++ b/Assets/Mirage/Runtime/NetworkBehaviour.cs @@ -536,23 +536,10 @@ internal void ResetSyncObjects() } #region RPC - // todo move this to NetworkIdentity to optimize (add a registermethod on NB that NI will call) - - // overriden by weaver + // overridden by weaver protected internal virtual int GetRpcCount() => 0; - - /// - /// Collection that holds information about all RPC in this networkbehaviour (including derived classes) - /// Can be used to get RPC name from its index - /// NOTE: Weaver uses this collection to add rpcs, If adding your own rpc do at your own risk - /// - [NonSerialized] - public readonly RemoteCallCollection RemoteCallCollection; - - protected NetworkBehaviour() - { - RemoteCallCollection = new RemoteCallCollection(this); - } + // overridden by weaver + protected internal virtual void RegisterRpc(RemoteCallCollection collection) { } #endregion public struct Id : IEquatable diff --git a/Assets/Mirage/Runtime/NetworkIdentity.cs b/Assets/Mirage/Runtime/NetworkIdentity.cs index e1de3b59ef..2e191168c9 100644 --- a/Assets/Mirage/Runtime/NetworkIdentity.cs +++ b/Assets/Mirage/Runtime/NetworkIdentity.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Mirage.Events; using Mirage.Logging; +using Mirage.RemoteCalls; using Mirage.Serialization; using UnityEngine; using UnityEngine.Serialization; @@ -247,7 +248,7 @@ internal void SetOwner(INetworkPlayer player) // if authority changes, we need to check if we are still allowed to sync to/from this instance foreach (var comp in NetworkBehaviours) comp.UpdateSyncObjectShouldSync(); - + _onOwnerChanged.Invoke(_owner); // only invoke again if new owner is not null @@ -1196,5 +1197,30 @@ public override string ToString() { return $"Identity[{NetId}, {name}]"; } + + + // todo update comment + /// + /// Collection that holds information about all RPC in this networkbehaviour (including derived classes) + /// Can be used to get RPC name from its index + /// NOTE: Weaver uses this collection to add rpcs, If adding your own rpc do at your own risk + /// + [NonSerialized] + private RemoteCallCollection _remoteCallCollection; + internal RemoteCallCollection RemoteCallCollection + { + get + { + if (_remoteCallCollection == null) + { + // we shoulld be save to lazy init + // we only need to register RPCs when we receive them + // when sending the index is baked in by weaver + _remoteCallCollection = new RemoteCallCollection(); + _remoteCallCollection.RegisterAll(NetworkBehaviours); + } + return _remoteCallCollection; + } + } } } diff --git a/Assets/Mirage/Runtime/RemoteCalls/ClientRpcSender.cs b/Assets/Mirage/Runtime/RemoteCalls/ClientRpcSender.cs index fbd3d026ba..39e1e57b3f 100644 --- a/Assets/Mirage/Runtime/RemoteCalls/ClientRpcSender.cs +++ b/Assets/Mirage/Runtime/RemoteCalls/ClientRpcSender.cs @@ -43,25 +43,23 @@ public static void SendTarget(NetworkBehaviour behaviour, int index, NetworkWrit [MethodImpl(MethodImplOptions.AggressiveInlining)] private static RpcMessage CreateMessage(NetworkBehaviour behaviour, int index, NetworkWriter writer) { - var rpc = behaviour.RemoteCallCollection.Get(index); - - Validate(behaviour, rpc); + Validate(behaviour, index); var message = new RpcMessage { NetId = behaviour.NetId, - ComponentIndex = behaviour.ComponentIndex, FunctionIndex = index, Payload = writer.ToArraySegment() }; return message; } - private static void Validate(NetworkBehaviour behaviour, RemoteCall rpc) + private static void Validate(NetworkBehaviour behaviour, int index) { var server = behaviour.Server; if (server == null || !server.Active) { + var rpc = behaviour.Identity.RemoteCallCollection.GetRelative(behaviour, index); throw new InvalidOperationException($"RPC Function {rpc} called when server is not active."); } } diff --git a/Assets/Mirage/Runtime/RemoteCalls/RemoteCallHelper.cs b/Assets/Mirage/Runtime/RemoteCalls/RemoteCallHelper.cs index 0cc01bef6d..5ab518466e 100644 --- a/Assets/Mirage/Runtime/RemoteCalls/RemoteCallHelper.cs +++ b/Assets/Mirage/Runtime/RemoteCalls/RemoteCallHelper.cs @@ -10,21 +10,43 @@ public class RemoteCallCollection { private static readonly ILogger logger = LogFactory.GetLogger(typeof(RemoteCallCollection)); - public RemoteCall[] remoteCalls; + /// + /// This is set by NetworkIdentity when we register each NetworkBehaviour so that they can pass their own idnex in + /// + public int[] IndexOffset; + public RemoteCall[] RemoteCalls; - public RemoteCallCollection(NetworkBehaviour behaviour) + public unsafe void RegisterAll(NetworkBehaviour[] behaviours) { - remoteCalls = new RemoteCall[behaviour.GetRpcCount()]; + var behaviourCount = behaviours.Length; + var totalCount = 0; + var counts = stackalloc int[behaviourCount]; + IndexOffset = new int[behaviourCount]; + for (var i = 0; i < behaviourCount; i++) + { + counts[i] = behaviours[i].GetRpcCount(); + totalCount += counts[i]; + + if (i > 0) + IndexOffset[i] = IndexOffset[i - 1] + counts[i - 1]; + } + + RemoteCalls = new RemoteCall[totalCount]; + for (var i = 0; i < behaviourCount; i++) + { + behaviours[i].RegisterRpc(this); + } } - public void Register(int index, Type invokeClass, string name, RpcInvokeType invokerType, RpcDelegate func, bool cmdRequireAuthority) + public void Register(int index, string name, bool cmdRequireAuthority, RpcInvokeType invokerType, NetworkBehaviour behaviour, RpcDelegate func) { + var indexOffset = GetIndexOffset(behaviour); // weaver gives index, so should never give 2 indexes that are the same - if (remoteCalls[index] != null) + if (RemoteCalls[indexOffset + index] != null) throw new InvalidOperationException("2 Rpc has same index"); - var call = new RemoteCall(invokeClass, invokerType, func, cmdRequireAuthority, name); - remoteCalls[index] = call; + var call = new RemoteCall(behaviour, invokerType, func, cmdRequireAuthority, name); + RemoteCalls[indexOffset + index] = call; if (logger.LogEnabled()) { @@ -33,7 +55,7 @@ public void Register(int index, Type invokeClass, string name, RpcInvokeType inv } } - public void RegisterRequest(int index, Type invokeClass, string name, RequestDelegate func, bool cmdRequireAuthority) + public void RegisterRequest(int index, string name, bool cmdRequireAuthority, NetworkBehaviour behaviour, RequestDelegate func) { async UniTaskVoid Wrapper(NetworkBehaviour obj, NetworkReader reader, INetworkPlayer senderPlayer, int replyId) { @@ -58,12 +80,22 @@ void CmdWrapper(NetworkBehaviour obj, NetworkReader reader, INetworkPlayer sende Wrapper(obj, reader, senderPlayer, replyId).Forget(); } - Register(index, invokeClass, name, RpcInvokeType.ServerRpc, CmdWrapper, cmdRequireAuthority); + Register(index, name, cmdRequireAuthority, RpcInvokeType.ServerRpc, behaviour, CmdWrapper); + } + + public int GetIndexOffset(NetworkBehaviour behaviour) + { + return IndexOffset[behaviour.ComponentIndex]; } - public RemoteCall Get(int index) + public RemoteCall GetRelative(NetworkBehaviour behaviour, int index) { - return remoteCalls[index]; + return RemoteCalls[GetIndexOffset(behaviour) + index]; + } + + public RemoteCall GetAbsolute(int index) + { + return RemoteCalls[index]; } } /// @@ -97,7 +129,7 @@ public class RemoteCall /// /// Function to be invoked when receiving message /// - public readonly RpcDelegate function; + public readonly RpcDelegate Function; /// /// Used by ServerRpc /// @@ -105,54 +137,22 @@ public class RemoteCall /// /// User friendly name /// - public readonly string name; + public readonly string Name; + + public readonly NetworkBehaviour Behaviour; - public RemoteCall(Type declaringType, RpcInvokeType invokeType, RpcDelegate function, bool requireAuthority, string name) + public RemoteCall(NetworkBehaviour behaviour, RpcInvokeType invokeType, RpcDelegate function, bool requireAuthority, string name) { - DeclaringType = declaringType; + Behaviour = behaviour; InvokeType = invokeType; - this.function = function; + Function = function; RequireAuthority = requireAuthority; - this.name = name; - } - - public bool AreEqual(Type declaringType, RpcInvokeType invokeType, RpcDelegate function) - { - if (InvokeType != invokeType) - return false; - - if (declaringType.IsGenericType) - return AreEqualIgnoringGeneric(declaringType, function); - - return DeclaringType == declaringType - && this.function == function; - } - - private bool AreEqualIgnoringGeneric(Type declaringType, RpcDelegate function) - { - // if this.type not generic, then not equal - if (!DeclaringType.IsGenericType) - return false; - - // types must be in same assembly to be equal - if (DeclaringType.Assembly != declaringType.Assembly) - return false; - - Debug.Assert(declaringType == function.Method.DeclaringType); - Debug.Assert(DeclaringType == this.function.Method.DeclaringType); - - // we check Assembly above, so we know these 2 functions must be in same assmebly here - // - we can check Namespace and Name to acount generic check - // - weaver check to make sure method in type have unique hash - // - weaver appends hash to names, so overloads will have different hash/names - return DeclaringType.Namespace == declaringType.Namespace - && DeclaringType.Name == declaringType.Name - && this.function.Method.Name == function.Method.Name; + Name = name; } - internal void Invoke(NetworkReader reader, NetworkBehaviour invokingType, INetworkPlayer senderPlayer = null, int replyId = 0) + internal void Invoke(NetworkReader reader, INetworkPlayer senderPlayer = null, int replyId = 0) { - function(invokingType, reader, senderPlayer, replyId); + Function(Behaviour, reader, senderPlayer, replyId); } /// @@ -161,7 +161,7 @@ internal void Invoke(NetworkReader reader, NetworkBehaviour invokingType, INetwo /// public override string ToString() { - return name; + return Name; } } } diff --git a/Assets/Mirage/Runtime/RemoteCalls/ServerRpcSender.cs b/Assets/Mirage/Runtime/RemoteCalls/ServerRpcSender.cs index f9f5db20f8..86a2c62e8b 100644 --- a/Assets/Mirage/Runtime/RemoteCalls/ServerRpcSender.cs +++ b/Assets/Mirage/Runtime/RemoteCalls/ServerRpcSender.cs @@ -16,7 +16,6 @@ public static void Send(NetworkBehaviour behaviour, int index, NetworkWriter wri var message = new ServerRpcMessage { NetId = behaviour.NetId, - ComponentIndex = behaviour.ComponentIndex, FunctionIndex = index, Payload = writer.ToArraySegment() }; @@ -30,7 +29,6 @@ public static UniTask SendWithReturn(NetworkBehaviour behaviour, int index var message = new ServerRpcWithReplyMessage { NetId = behaviour.NetId, - ComponentIndex = behaviour.ComponentIndex, FunctionIndex = index, Payload = writer.ToArraySegment() }; @@ -46,17 +44,18 @@ public static UniTask SendWithReturn(NetworkBehaviour behaviour, int index private static void Validate(NetworkBehaviour behaviour, int index, bool requireAuthority) { - var rpc = behaviour.RemoteCallCollection.Get(index); var client = behaviour.Client; if (client == null || !client.Active) { + var rpc = behaviour.Identity.RemoteCallCollection.GetRelative(behaviour, index); throw new InvalidOperationException($"ServerRpc Function {rpc} called on server without an active client."); } // if authority is required, then client must have authority to send if (requireAuthority && !behaviour.HasAuthority) { + var rpc = behaviour.Identity.RemoteCallCollection.GetRelative(behaviour, index); throw new InvalidOperationException($"Trying to send ServerRpc for object without authority. {rpc}"); } diff --git a/Assets/Mirage/Runtime/ServerObjectManager.cs b/Assets/Mirage/Runtime/ServerObjectManager.cs index eff9faf22d..db2c695e97 100644 --- a/Assets/Mirage/Runtime/ServerObjectManager.cs +++ b/Assets/Mirage/Runtime/ServerObjectManager.cs @@ -322,16 +322,16 @@ private static void ThrowIfNoCharacter(INetworkPlayer player) /// private void OnServerRpcWithReplyMessage(INetworkPlayer player, ServerRpcWithReplyMessage msg) { - OnServerRpc(player, msg.NetId, msg.ComponentIndex, msg.FunctionIndex, msg.Payload, msg.ReplyId); + OnServerRpc(player, msg.NetId, msg.FunctionIndex, msg.Payload, msg.ReplyId); } private void OnServerRpcMessage(INetworkPlayer player, ServerRpcMessage msg) { - OnServerRpc(player, msg.NetId, msg.ComponentIndex, msg.FunctionIndex, msg.Payload, default); + OnServerRpc(player, msg.NetId, msg.FunctionIndex, msg.Payload, default); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void OnServerRpc(INetworkPlayer player, uint netId, int componentIndex, int functionIndex, ArraySegment payload, int replyId) + private void OnServerRpc(INetworkPlayer player, uint netId, int functionIndex, ArraySegment payload, int replyId) { if (!_server.World.TryGetIdentity(netId, out var identity)) { @@ -339,9 +339,7 @@ private void OnServerRpc(INetworkPlayer player, uint netId, int componentIndex, return; } - var behaviour = identity.NetworkBehaviours[componentIndex]; - - var remoteCall = behaviour.RemoteCallCollection.Get(functionIndex); + var remoteCall = identity.RemoteCallCollection.GetAbsolute(functionIndex); if (remoteCall.InvokeType != RpcInvokeType.ServerRpc) { @@ -361,7 +359,7 @@ private void OnServerRpc(INetworkPlayer player, uint netId, int componentIndex, using (var reader = NetworkReaderPool.GetReader(payload, _server.World)) { - remoteCall.Invoke(reader, behaviour, player, replyId); + remoteCall.Invoke(reader, player, replyId); } } diff --git a/Assets/Mirage/Weaver/Processors/NetworkBehaviour/BaseMethodHelper.cs b/Assets/Mirage/Weaver/Processors/NetworkBehaviour/BaseMethodHelper.cs index 64fd53922a..0d9f6519be 100644 --- a/Assets/Mirage/Weaver/Processors/NetworkBehaviour/BaseMethodHelper.cs +++ b/Assets/Mirage/Weaver/Processors/NetworkBehaviour/BaseMethodHelper.cs @@ -22,7 +22,7 @@ public BaseMethodHelper(ModuleDefinition module, TypeDefinition typeDefinition) } /// - /// Adds Serialize method to current type + /// Adds method to current type /// /// public void AddMethod() diff --git a/Assets/Mirage/Weaver/Processors/NetworkBehaviour/RegisterRpcHelper.cs b/Assets/Mirage/Weaver/Processors/NetworkBehaviour/RegisterRpcHelper.cs new file mode 100644 index 0000000000..35fa34642d --- /dev/null +++ b/Assets/Mirage/Weaver/Processors/NetworkBehaviour/RegisterRpcHelper.cs @@ -0,0 +1,23 @@ +using Mirage.RemoteCalls; +using Mono.Cecil; + +namespace Mirage.Weaver.NetworkBehaviours +{ + internal class RegisterRpcHelper : BaseMethodHelper + { + public RegisterRpcHelper(ModuleDefinition module, TypeDefinition typeDefinition) : base(module, typeDefinition) + { + } + + public override string MethodName => nameof(NetworkBehaviour.RegisterRpc); + + protected override void AddParameters() + { + Method.AddParam("collection"); + } + + protected override void AddLocals() + { + } + } +} diff --git a/Assets/Mirage/Weaver/Processors/NetworkBehaviour/RegisterRpcHelper.cs.meta b/Assets/Mirage/Weaver/Processors/NetworkBehaviour/RegisterRpcHelper.cs.meta new file mode 100644 index 0000000000..5ae3b281b9 --- /dev/null +++ b/Assets/Mirage/Weaver/Processors/NetworkBehaviour/RegisterRpcHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de70d783af07b8844a43217d0398f27f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirage/Weaver/Processors/NetworkBehaviourProcessor.cs b/Assets/Mirage/Weaver/Processors/NetworkBehaviourProcessor.cs index 9bbd516202..fe4bd31e0f 100644 --- a/Assets/Mirage/Weaver/Processors/NetworkBehaviourProcessor.cs +++ b/Assets/Mirage/Weaver/Processors/NetworkBehaviourProcessor.cs @@ -91,13 +91,24 @@ public static void MarkAsProcessed(TypeDefinition td) private void RegisterRpcs(List rpcs) { + Weaver.DebugLog(netBehaviourSubclass, "Set const RPC Count"); SetRpcCount(rpcs.Count); - Weaver.DebugLog(netBehaviourSubclass, " GenerateConstants "); - netBehaviourSubclass.AddToConstructor(logger, (worker) => - { - RegisterRpc.RegisterAll(worker, rpcs); - }); + // if there are no rpcs then we dont need to override method + if (rpcs.Count == 0) + return; + + Weaver.DebugLog(netBehaviourSubclass, "Override RegisterRPC"); + + var helper = new RegisterRpcHelper(netBehaviourSubclass.Module, netBehaviourSubclass); + if (helper.HasManualOverride()) + throw new RpcException($"{helper.MethodName} should not have a manual override", helper.GetManualOverride()); + + helper.AddMethod(); + + RegisterRpc.RegisterAll(helper.Worker, rpcs); + + helper.Worker.Emit(OpCodes.Ret); } private void SetRpcCount(int count) diff --git a/Assets/Mirage/Weaver/Processors/RegisterRpc.cs b/Assets/Mirage/Weaver/Processors/RegisterRpc.cs index 06a21c8c57..59a3ccd58c 100644 --- a/Assets/Mirage/Weaver/Processors/RegisterRpc.cs +++ b/Assets/Mirage/Weaver/Processors/RegisterRpc.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using Cysharp.Threading.Tasks; using Mirage.RemoteCalls; using Mono.Cecil; @@ -95,39 +94,27 @@ private static void CallRegister(ILProcessor worker, RpcMethod rpcMethod, RpcInv var skeleton = rpcMethod.skeleton; var name = HumanReadableName(rpcMethod.stub); var index = rpcMethod.Index; - var module = rpcMethod.stub.Module; - var collectionFieldInfo = typeof(NetworkBehaviour).GetField(nameof(NetworkBehaviour.RemoteCallCollection), BindingFlags.Public | BindingFlags.Instance); - var collectionField = module.ImportReference(collectionFieldInfo); - - // arg0 is remote collection - // this.remoteCallCollection - worker.Append(worker.Create(OpCodes.Ldarg_0)); - worker.Append(worker.Create(OpCodes.Ldfld, collectionField)); - - // arg1 is rpc index + // write collection.Register(index, name, invokerType, cmdRequireAuthority,... + // arg1 is rpc collection + worker.Append(worker.Create(OpCodes.Ldarg_1)); worker.Append(worker.Create(OpCodes.Ldc_I4, index)); - - // typeof() - var netBehaviourSubclass = skeleton.DeclaringType.ConvertToGenericIfNeeded(); - worker.Append(worker.Create(OpCodes.Ldtoken, netBehaviourSubclass)); - worker.Append(worker.Create(OpCodes.Call, () => Type.GetTypeFromHandle(default))); - worker.Append(worker.Create(OpCodes.Ldstr, name)); - + worker.Append(worker.Create(requireAuthority.OpCode_Ldc())); // RegisterRequest has no type, it is always serverRpc, so dont need to include arg if (invokeType.HasValue) - { worker.Append(worker.Create(OpCodes.Ldc_I4, (int)invokeType.Value)); - } - // new delegate + // write behaviour + worker.Append(worker.Create(OpCodes.Ldarg_0)); + + // create delegate as last arg worker.Append(worker.Create(OpCodes.Ldnull)); worker.Append(worker.Create(OpCodes.Ldftn, skeleton.MakeHostInstanceSelfGeneric())); var @delegate = CreateRpcDelegate(skeleton); worker.Append(worker.Create(OpCodes.Newobj, @delegate)); - worker.Append(worker.Create(requireAuthority.OpCode_Ldc())); + // invoke register worker.Append(worker.Create(OpCodes.Call, registerMethod)); } diff --git a/Assets/Tests/Runtime/Serialization/MessageTests.cs b/Assets/Tests/Runtime/Serialization/MessageTests.cs index d576630844..3ea3a72d43 100644 --- a/Assets/Tests/Runtime/Serialization/MessageTests.cs +++ b/Assets/Tests/Runtime/Serialization/MessageTests.cs @@ -17,7 +17,6 @@ public void ServerRpcMessageTest() var message = new ServerRpcMessage { NetId = 42, - ComponentIndex = 4, FunctionIndex = 2, Payload = new ArraySegment(new byte[] { 0x01, 0x02 }) }; @@ -26,7 +25,6 @@ public void ServerRpcMessageTest() // deserialize the same data - do we get the same result? var fresh = MessagePacker.Unpack(arr, null); Assert.That(fresh.NetId, Is.EqualTo(message.NetId)); - Assert.That(fresh.ComponentIndex, Is.EqualTo(message.ComponentIndex)); Assert.That(fresh.FunctionIndex, Is.EqualTo(message.FunctionIndex)); Assert.That(fresh.Payload, Has.Count.EqualTo(message.Payload.Count)); for (var i = 0; i < fresh.Payload.Count; ++i) @@ -105,14 +103,12 @@ public void RpcMessageTest() var message = new RpcMessage { NetId = 42, - ComponentIndex = 4, FunctionIndex = 3, Payload = new ArraySegment(new byte[] { 0x01, 0x02 }) }; var arr = MessagePacker.Pack(message); var fresh = MessagePacker.Unpack(arr, null); Assert.That(fresh.NetId, Is.EqualTo(message.NetId)); - Assert.That(fresh.ComponentIndex, Is.EqualTo(message.ComponentIndex)); Assert.That(fresh.FunctionIndex, Is.EqualTo(message.FunctionIndex)); Assert.That(fresh.Payload.Count, Is.EqualTo(message.Payload.Count)); for (var i = 0; i < fresh.Payload.Count; ++i)