From 698fd2de5b2cd650ddfc7ca9a027964c62ee226e Mon Sep 17 00:00:00 2001 From: xmanning <166966460+xmanning@users.noreply.github.com> Date: Sat, 27 Sep 2025 16:26:57 -0400 Subject: [PATCH 01/27] Secure RpcMessage against SenderClientId spoofing When a server receives an RpcMessage, it should update the SenderClientId to match the SenderId provided by the messaging manager. This is to prevent modified clients from spoofing their SenderClientId as other clients. --- .../Runtime/Messaging/Messages/RpcMessages.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs index 70e4c2aadf..7cff3611c3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs @@ -196,6 +196,12 @@ public unsafe bool Deserialize(FastBufferReader reader, ref NetworkContext conte public void Handle(ref NetworkContext context) { + var networkManager = (NetworkManager)context.SystemOwner; + if (networkManager.IsServer) + { + SenderClientId = context.SenderId; + } + var rpcParams = new __RpcParams { Ext = new RpcParams From 72f0e82ee14775181cf3cd0d95a84fb1e0bef573 Mon Sep 17 00:00:00 2001 From: xmanning Date: Wed, 1 Oct 2025 23:03:50 -0400 Subject: [PATCH 02/27] Replaced RequireOwnership with a new RpcInvokePermission that is respected from the server for both direct and proxy RPCs. --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 62 +++++++++++++++++-- .../Runtime/Core/NetworkBehaviour.cs | 12 +++- .../Messaging/Messages/ProxyMessage.cs | 24 ++++++- .../Runtime/Messaging/Messages/RpcMessages.cs | 19 +++++- .../Runtime/Messaging/RpcAttributes.cs | 29 +++++++-- 5 files changed, 130 insertions(+), 16 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index 894aaf76d5..83c40ab7de 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -609,6 +609,7 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly, private const string k_NetworkVariableBase_Initialize = nameof(NetworkVariableBase.Initialize); private const string k_RpcAttribute_Delivery = nameof(RpcAttribute.Delivery); + private const string k_RpcAttribute_InvokePermission = nameof(RpcAttribute.InvokePermission); private const string k_ServerRpcAttribute_RequireOwnership = nameof(ServerRpcAttribute.RequireOwnership); private const string k_RpcParams_Server = nameof(__RpcParams.Server); private const string k_RpcParams_Client = nameof(__RpcParams.Client); @@ -1311,7 +1312,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass return; } } - var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler, string RpcMethodName)>(); + var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler, string RpcMethodName, CustomAttribute rpcAttribute)>(); bool isEditorOrDevelopment = assemblyDefines.Contains("UNITY_EDITOR") || assemblyDefines.Contains("DEVELOPMENT_BUILD"); @@ -1342,7 +1343,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass InjectWriteAndCallBlocks(methodDefinition, rpcAttribute, rpcMethodId); - rpcHandlers.Add((rpcMethodId, GenerateStaticHandler(methodDefinition, rpcAttribute, rpcMethodId), methodDefinition.Name)); + rpcHandlers.Add((rpcMethodId, GenerateStaticHandler(methodDefinition, rpcAttribute, rpcMethodId), methodDefinition.Name, rpcAttribute)); } GenerateVariableInitialization(typeDefinition); @@ -1424,7 +1425,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass var instructions = new List(); var processor = initializeRpcsMethodDef.Body.GetILProcessor(); - foreach (var (rpcMethodId, rpcHandler, rpcMethodName) in rpcHandlers) + foreach (var (rpcMethodId, rpcHandler, rpcMethodName, rpcAttribute) in rpcHandlers) { typeDefinition.Methods.Add(rpcHandler); @@ -1439,12 +1440,25 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass callMethod = callMethod.MakeGeneric(genericTypes.ToArray()); } + RpcInvokePermission invokePermission = RpcInvokePermission.Anyone; + + foreach (var attrField in rpcAttribute.Fields) + { + switch (attrField.Name) + { + case k_RpcAttribute_InvokePermission: + invokePermission = (RpcInvokePermission)attrField.Argument.Value; + break; + } + } + // __registerRpc(RpcMethodId, HandleFunc, methodName); instructions.Add(processor.Create(OpCodes.Ldarg_0)); instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId))); instructions.Add(processor.Create(OpCodes.Ldnull)); instructions.Add(processor.Create(OpCodes.Ldftn, callMethod)); instructions.Add(processor.Create(OpCodes.Newobj, m_NetworkHandlerDelegateCtor_MethodRef)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)invokePermission)); instructions.Add(processor.Create(OpCodes.Ldstr, rpcMethodName)); instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour___registerRpc_MethodRef)); } @@ -2851,13 +2865,13 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; var isCientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; var isGenericRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.RpcAttribute_FullName; - var requireOwnership = true; // default value MUST be == `ServerRpcAttribute.RequireOwnership` + var invokePermission = RpcInvokePermission.Anyone; // default value MUST be == `ServerRpcAttribute.RequireOwnership` foreach (var attrField in rpcAttribute.Fields) { switch (attrField.Name) { case k_ServerRpcAttribute_RequireOwnership: - requireOwnership = attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value; + invokePermission = (attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value) ? RpcInvokePermission.Owner : RpcInvokePermission.Anyone; break; } } @@ -2887,7 +2901,7 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition processor.Append(lastInstr); } - if (isServerRpc && requireOwnership) + if (isServerRpc && invokePermission == RpcInvokePermission.Owner) { var roReturnInstr = processor.Create(OpCodes.Ret); var roLastInstr = processor.Create(OpCodes.Nop); @@ -2921,6 +2935,42 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition processor.Append(logNextInstr); + processor.Append(roReturnInstr); + processor.Append(roLastInstr); + } else if (invokePermission == RpcInvokePermission.Server) + { + var roReturnInstr = processor.Create(OpCodes.Ret); + var roLastInstr = processor.Create(OpCodes.Nop); + + // if (rpcParams.Server.Receive.SenderClientId != NetworkManager.IsServer) { ... } return; + processor.Emit(OpCodes.Ldarg_2); + processor.Emit(OpCodes.Ldfld, m_RpcParams_Server_FieldRef); + processor.Emit(OpCodes.Ldfld, m_ServerRpcParams_Receive_FieldRef); + processor.Emit(OpCodes.Ldfld, m_ServerRpcParams_Receive_SenderClientId_FieldRef); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Call, m_NetworkManager_getIsServer_MethodRef); + processor.Emit(OpCodes.Ceq); + processor.Emit(OpCodes.Ldc_I4, 0); + processor.Emit(OpCodes.Ceq); + processor.Emit(OpCodes.Brfalse, roLastInstr); + + var logNextInstr = processor.Create(OpCodes.Nop); + + // if (LogLevel.Normal > networkManager.LogLevel) + processor.Emit(OpCodes.Ldloc, netManLocIdx); + processor.Emit(OpCodes.Ldfld, m_NetworkManager_LogLevel_FieldRef); + processor.Emit(OpCodes.Ldc_I4, (int)LogLevel.Normal); + processor.Emit(OpCodes.Cgt); + processor.Emit(OpCodes.Ldc_I4, 0); + processor.Emit(OpCodes.Ceq); + processor.Emit(OpCodes.Brfalse, logNextInstr); + + // Debug.LogError(...); + processor.Emit(OpCodes.Ldstr, "Only the server can invoke an Rpc with RpcInvokePermission.Server!"); + processor.Emit(OpCodes.Call, m_Debug_LogError_MethodRef); + + processor.Append(logNextInstr); + processor.Append(roReturnInstr); processor.Append(roLastInstr); } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 332694af7d..c3e599d541 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -39,6 +39,7 @@ public abstract class NetworkBehaviour : MonoBehaviour // RuntimeAccessModifiersILPP will make this `public` internal static readonly Dictionary> __rpc_func_table = new Dictionary>(); + internal static readonly Dictionary> __rpc_permission_table = new Dictionary>(); #if DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE // RuntimeAccessModifiersILPP will make this `public` @@ -326,12 +327,15 @@ internal FastBufferWriter __beginSendRpc(uint rpcMethodId, RpcParams rpcParams, #pragma warning restore IDE1006 // restore naming rule violation check { if (m_NetworkObject == null && !IsSpawned) - { + { throw new RpcException("The NetworkBehaviour must be spawned before calling this method."); } - if (attributeParams.RequireOwnership && !IsOwner) + if (attributeParams.InvokePermission == RpcInvokePermission.Owner && !IsOwner) { throw new RpcException("This RPC can only be sent by its owner."); + } else if (attributeParams.InvokePermission == RpcInvokePermission.Server && !IsServer) + { + throw new RpcException("This RPC can only be sent by the server."); } return new FastBufferWriter(k_RpcMessageDefaultSize, Allocator.Temp, k_RpcMessageMaximumSize); } @@ -950,10 +954,11 @@ internal virtual void __initializeRpcs() #pragma warning disable IDE1006 // disable naming rule violation check // RuntimeAccessModifiersILPP will make this `protected` - internal void __registerRpc(uint hash, RpcReceiveHandler handler, string rpcMethodName) + internal void __registerRpc(uint hash, RpcReceiveHandler handler, RpcInvokePermission permission, string rpcMethodName) #pragma warning restore IDE1006 // restore naming rule violation check { __rpc_func_table[GetType()][hash] = handler; + __rpc_permission_table[GetType()][hash] = permission; #if DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE __rpc_name_table[GetType()][hash] = rpcMethodName; #endif @@ -1000,6 +1005,7 @@ internal void InitializeVariables() if (!__rpc_func_table.ContainsKey(GetType())) { __rpc_func_table[GetType()] = new Dictionary(); + __rpc_permission_table[GetType()] = new Dictionary(); #if UNITY_EDITOR || DEVELOPMENT_BUILD || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE __rpc_name_table[GetType()] = new Dictionary(); #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs index 57c8345175..2b7346c062 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs @@ -41,9 +41,31 @@ public unsafe void Handle(ref NetworkContext context) } return; } - var observers = networkObject.Observers; + var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(WrappedMessage.Metadata.NetworkBehaviourId); + + RpcInvokePermission permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][WrappedMessage.Metadata.NetworkRpcMethodId]; + bool hasPermission = permission switch + { + RpcInvokePermission.Anyone => true, + RpcInvokePermission.Server => context.SenderId == networkManager.LocalClientId, + RpcInvokePermission.Owner => context.SenderId == networkBehaviour.OwnerClientId, + _ => false, + }; + + // Do not handle the message if the sender does not have permission to do so. + if (!hasPermission) + { + return; + } + + if (networkManager.IsServer) + { + WrappedMessage.SenderClientId = context.SenderId; + } + + var nonServerIds = new NativeList(Allocator.Temp); for (var i = 0; i < TargetClientIds.Length; ++i) { diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs index 7cff3611c3..2b74f13186 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs @@ -66,12 +66,29 @@ public static void Handle(ref NetworkContext context, ref RpcMetadata metadata, { NetworkLog.LogWarning($"[{metadata.NetworkObjectId}, {metadata.NetworkBehaviourId}, {metadata.NetworkRpcMethodId}] An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); } - return; } var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(metadata.NetworkBehaviourId); try { + Type type = networkBehaviour.GetType(); + if (networkManager.IsServer) + { + RpcInvokePermission permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId]; + bool hasPermission = permission switch + { + RpcInvokePermission.Anyone => true, + RpcInvokePermission.Server => context.SenderId == networkManager.LocalClientId, + RpcInvokePermission.Owner => context.SenderId == networkBehaviour.OwnerClientId, + _ => false, + }; + + // Do not handle the message if the sender does not have permission to do so. + if (!hasPermission) + { + return; + } + } NetworkBehaviour.__rpc_func_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId](networkBehaviour, payload, rpcParams); } catch (Exception ex) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs index 6d276ab580..1e9b8c634e 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs @@ -18,6 +18,25 @@ public enum RpcDelivery Unreliable } + /// + /// RPC invoke permissions + /// + public enum RpcInvokePermission + { + /// + /// Anyone can invoke the Rpc. + /// + Anyone = 0, + /// + /// Rpc can only be invoked by the server. + /// + Server, + /// + /// Rpc can only be invoked by the owner of the NetworkBehaviour. + /// + Owner, + } + /// /// Represents the common base class for Rpc attributes. /// @@ -35,9 +54,9 @@ public struct RpcAttributeParams public RpcDelivery Delivery; /// - /// When true, only the owner of the object can execute this RPC + /// Who has network permission to invoke this RPC /// - public bool RequireOwnership; + public RpcInvokePermission InvokePermission; /// /// When true, local execution of the RPC is deferred until the next network tick @@ -57,9 +76,9 @@ public struct RpcAttributeParams public RpcDelivery Delivery = RpcDelivery.Reliable; /// - /// When true, only the owner of the object can execute this RPC + /// Who has network permission to invoke this RPC /// - public bool RequireOwnership; + public RpcInvokePermission InvokePermission; /// /// When true, local execution of the RPC is deferred until the next network tick @@ -97,7 +116,7 @@ public class ServerRpcAttribute : RpcAttribute /// When true, only the owner of the NetworkObject can invoke this ServerRpc. /// This property overrides the base RpcAttribute.RequireOwnership. /// - public new bool RequireOwnership; + public bool RequireOwnership; /// /// Initializes a new instance of ServerRpcAttribute that targets the server From a0ade5b055d2535d866930d35e9bcd8f1cbe8c27 Mon Sep 17 00:00:00 2001 From: xmanning Date: Wed, 1 Oct 2025 23:03:50 -0400 Subject: [PATCH 03/27] Replaced RequireOwnership with a new RpcInvokePermission that is respected from the server for both direct and proxy RPCs. --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 62 +++++++++++++++++-- .../Runtime/Core/NetworkBehaviour.cs | 12 +++- .../Messaging/Messages/ProxyMessage.cs | 24 ++++++- .../Runtime/Messaging/Messages/RpcMessages.cs | 19 +++++- .../Runtime/Messaging/RpcAttributes.cs | 29 +++++++-- 5 files changed, 130 insertions(+), 16 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index 894aaf76d5..83c40ab7de 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -609,6 +609,7 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly, private const string k_NetworkVariableBase_Initialize = nameof(NetworkVariableBase.Initialize); private const string k_RpcAttribute_Delivery = nameof(RpcAttribute.Delivery); + private const string k_RpcAttribute_InvokePermission = nameof(RpcAttribute.InvokePermission); private const string k_ServerRpcAttribute_RequireOwnership = nameof(ServerRpcAttribute.RequireOwnership); private const string k_RpcParams_Server = nameof(__RpcParams.Server); private const string k_RpcParams_Client = nameof(__RpcParams.Client); @@ -1311,7 +1312,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass return; } } - var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler, string RpcMethodName)>(); + var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler, string RpcMethodName, CustomAttribute rpcAttribute)>(); bool isEditorOrDevelopment = assemblyDefines.Contains("UNITY_EDITOR") || assemblyDefines.Contains("DEVELOPMENT_BUILD"); @@ -1342,7 +1343,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass InjectWriteAndCallBlocks(methodDefinition, rpcAttribute, rpcMethodId); - rpcHandlers.Add((rpcMethodId, GenerateStaticHandler(methodDefinition, rpcAttribute, rpcMethodId), methodDefinition.Name)); + rpcHandlers.Add((rpcMethodId, GenerateStaticHandler(methodDefinition, rpcAttribute, rpcMethodId), methodDefinition.Name, rpcAttribute)); } GenerateVariableInitialization(typeDefinition); @@ -1424,7 +1425,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass var instructions = new List(); var processor = initializeRpcsMethodDef.Body.GetILProcessor(); - foreach (var (rpcMethodId, rpcHandler, rpcMethodName) in rpcHandlers) + foreach (var (rpcMethodId, rpcHandler, rpcMethodName, rpcAttribute) in rpcHandlers) { typeDefinition.Methods.Add(rpcHandler); @@ -1439,12 +1440,25 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass callMethod = callMethod.MakeGeneric(genericTypes.ToArray()); } + RpcInvokePermission invokePermission = RpcInvokePermission.Anyone; + + foreach (var attrField in rpcAttribute.Fields) + { + switch (attrField.Name) + { + case k_RpcAttribute_InvokePermission: + invokePermission = (RpcInvokePermission)attrField.Argument.Value; + break; + } + } + // __registerRpc(RpcMethodId, HandleFunc, methodName); instructions.Add(processor.Create(OpCodes.Ldarg_0)); instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId))); instructions.Add(processor.Create(OpCodes.Ldnull)); instructions.Add(processor.Create(OpCodes.Ldftn, callMethod)); instructions.Add(processor.Create(OpCodes.Newobj, m_NetworkHandlerDelegateCtor_MethodRef)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)invokePermission)); instructions.Add(processor.Create(OpCodes.Ldstr, rpcMethodName)); instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour___registerRpc_MethodRef)); } @@ -2851,13 +2865,13 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; var isCientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; var isGenericRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.RpcAttribute_FullName; - var requireOwnership = true; // default value MUST be == `ServerRpcAttribute.RequireOwnership` + var invokePermission = RpcInvokePermission.Anyone; // default value MUST be == `ServerRpcAttribute.RequireOwnership` foreach (var attrField in rpcAttribute.Fields) { switch (attrField.Name) { case k_ServerRpcAttribute_RequireOwnership: - requireOwnership = attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value; + invokePermission = (attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value) ? RpcInvokePermission.Owner : RpcInvokePermission.Anyone; break; } } @@ -2887,7 +2901,7 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition processor.Append(lastInstr); } - if (isServerRpc && requireOwnership) + if (isServerRpc && invokePermission == RpcInvokePermission.Owner) { var roReturnInstr = processor.Create(OpCodes.Ret); var roLastInstr = processor.Create(OpCodes.Nop); @@ -2921,6 +2935,42 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition processor.Append(logNextInstr); + processor.Append(roReturnInstr); + processor.Append(roLastInstr); + } else if (invokePermission == RpcInvokePermission.Server) + { + var roReturnInstr = processor.Create(OpCodes.Ret); + var roLastInstr = processor.Create(OpCodes.Nop); + + // if (rpcParams.Server.Receive.SenderClientId != NetworkManager.IsServer) { ... } return; + processor.Emit(OpCodes.Ldarg_2); + processor.Emit(OpCodes.Ldfld, m_RpcParams_Server_FieldRef); + processor.Emit(OpCodes.Ldfld, m_ServerRpcParams_Receive_FieldRef); + processor.Emit(OpCodes.Ldfld, m_ServerRpcParams_Receive_SenderClientId_FieldRef); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Call, m_NetworkManager_getIsServer_MethodRef); + processor.Emit(OpCodes.Ceq); + processor.Emit(OpCodes.Ldc_I4, 0); + processor.Emit(OpCodes.Ceq); + processor.Emit(OpCodes.Brfalse, roLastInstr); + + var logNextInstr = processor.Create(OpCodes.Nop); + + // if (LogLevel.Normal > networkManager.LogLevel) + processor.Emit(OpCodes.Ldloc, netManLocIdx); + processor.Emit(OpCodes.Ldfld, m_NetworkManager_LogLevel_FieldRef); + processor.Emit(OpCodes.Ldc_I4, (int)LogLevel.Normal); + processor.Emit(OpCodes.Cgt); + processor.Emit(OpCodes.Ldc_I4, 0); + processor.Emit(OpCodes.Ceq); + processor.Emit(OpCodes.Brfalse, logNextInstr); + + // Debug.LogError(...); + processor.Emit(OpCodes.Ldstr, "Only the server can invoke an Rpc with RpcInvokePermission.Server!"); + processor.Emit(OpCodes.Call, m_Debug_LogError_MethodRef); + + processor.Append(logNextInstr); + processor.Append(roReturnInstr); processor.Append(roLastInstr); } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 332694af7d..c3e599d541 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -39,6 +39,7 @@ public abstract class NetworkBehaviour : MonoBehaviour // RuntimeAccessModifiersILPP will make this `public` internal static readonly Dictionary> __rpc_func_table = new Dictionary>(); + internal static readonly Dictionary> __rpc_permission_table = new Dictionary>(); #if DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE // RuntimeAccessModifiersILPP will make this `public` @@ -326,12 +327,15 @@ internal FastBufferWriter __beginSendRpc(uint rpcMethodId, RpcParams rpcParams, #pragma warning restore IDE1006 // restore naming rule violation check { if (m_NetworkObject == null && !IsSpawned) - { + { throw new RpcException("The NetworkBehaviour must be spawned before calling this method."); } - if (attributeParams.RequireOwnership && !IsOwner) + if (attributeParams.InvokePermission == RpcInvokePermission.Owner && !IsOwner) { throw new RpcException("This RPC can only be sent by its owner."); + } else if (attributeParams.InvokePermission == RpcInvokePermission.Server && !IsServer) + { + throw new RpcException("This RPC can only be sent by the server."); } return new FastBufferWriter(k_RpcMessageDefaultSize, Allocator.Temp, k_RpcMessageMaximumSize); } @@ -950,10 +954,11 @@ internal virtual void __initializeRpcs() #pragma warning disable IDE1006 // disable naming rule violation check // RuntimeAccessModifiersILPP will make this `protected` - internal void __registerRpc(uint hash, RpcReceiveHandler handler, string rpcMethodName) + internal void __registerRpc(uint hash, RpcReceiveHandler handler, RpcInvokePermission permission, string rpcMethodName) #pragma warning restore IDE1006 // restore naming rule violation check { __rpc_func_table[GetType()][hash] = handler; + __rpc_permission_table[GetType()][hash] = permission; #if DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE __rpc_name_table[GetType()][hash] = rpcMethodName; #endif @@ -1000,6 +1005,7 @@ internal void InitializeVariables() if (!__rpc_func_table.ContainsKey(GetType())) { __rpc_func_table[GetType()] = new Dictionary(); + __rpc_permission_table[GetType()] = new Dictionary(); #if UNITY_EDITOR || DEVELOPMENT_BUILD || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE __rpc_name_table[GetType()] = new Dictionary(); #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs index 57c8345175..2b7346c062 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs @@ -41,9 +41,31 @@ public unsafe void Handle(ref NetworkContext context) } return; } - var observers = networkObject.Observers; + var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(WrappedMessage.Metadata.NetworkBehaviourId); + + RpcInvokePermission permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][WrappedMessage.Metadata.NetworkRpcMethodId]; + bool hasPermission = permission switch + { + RpcInvokePermission.Anyone => true, + RpcInvokePermission.Server => context.SenderId == networkManager.LocalClientId, + RpcInvokePermission.Owner => context.SenderId == networkBehaviour.OwnerClientId, + _ => false, + }; + + // Do not handle the message if the sender does not have permission to do so. + if (!hasPermission) + { + return; + } + + if (networkManager.IsServer) + { + WrappedMessage.SenderClientId = context.SenderId; + } + + var nonServerIds = new NativeList(Allocator.Temp); for (var i = 0; i < TargetClientIds.Length; ++i) { diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs index 7cff3611c3..2b74f13186 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs @@ -66,12 +66,29 @@ public static void Handle(ref NetworkContext context, ref RpcMetadata metadata, { NetworkLog.LogWarning($"[{metadata.NetworkObjectId}, {metadata.NetworkBehaviourId}, {metadata.NetworkRpcMethodId}] An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); } - return; } var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(metadata.NetworkBehaviourId); try { + Type type = networkBehaviour.GetType(); + if (networkManager.IsServer) + { + RpcInvokePermission permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId]; + bool hasPermission = permission switch + { + RpcInvokePermission.Anyone => true, + RpcInvokePermission.Server => context.SenderId == networkManager.LocalClientId, + RpcInvokePermission.Owner => context.SenderId == networkBehaviour.OwnerClientId, + _ => false, + }; + + // Do not handle the message if the sender does not have permission to do so. + if (!hasPermission) + { + return; + } + } NetworkBehaviour.__rpc_func_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId](networkBehaviour, payload, rpcParams); } catch (Exception ex) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs index 6d276ab580..1e9b8c634e 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs @@ -18,6 +18,25 @@ public enum RpcDelivery Unreliable } + /// + /// RPC invoke permissions + /// + public enum RpcInvokePermission + { + /// + /// Anyone can invoke the Rpc. + /// + Anyone = 0, + /// + /// Rpc can only be invoked by the server. + /// + Server, + /// + /// Rpc can only be invoked by the owner of the NetworkBehaviour. + /// + Owner, + } + /// /// Represents the common base class for Rpc attributes. /// @@ -35,9 +54,9 @@ public struct RpcAttributeParams public RpcDelivery Delivery; /// - /// When true, only the owner of the object can execute this RPC + /// Who has network permission to invoke this RPC /// - public bool RequireOwnership; + public RpcInvokePermission InvokePermission; /// /// When true, local execution of the RPC is deferred until the next network tick @@ -57,9 +76,9 @@ public struct RpcAttributeParams public RpcDelivery Delivery = RpcDelivery.Reliable; /// - /// When true, only the owner of the object can execute this RPC + /// Who has network permission to invoke this RPC /// - public bool RequireOwnership; + public RpcInvokePermission InvokePermission; /// /// When true, local execution of the RPC is deferred until the next network tick @@ -97,7 +116,7 @@ public class ServerRpcAttribute : RpcAttribute /// When true, only the owner of the NetworkObject can invoke this ServerRpc. /// This property overrides the base RpcAttribute.RequireOwnership. /// - public new bool RequireOwnership; + public bool RequireOwnership; /// /// Initializes a new instance of ServerRpcAttribute that targets the server From fcfc662209e8cf0fb411451f7585cc6b995d868e Mon Sep 17 00:00:00 2001 From: xmanning Date: Thu, 2 Oct 2025 01:15:44 -0400 Subject: [PATCH 04/27] Add missing switch case --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index 83c40ab7de..a5449cdf40 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -2865,7 +2865,7 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; var isCientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; var isGenericRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.RpcAttribute_FullName; - var invokePermission = RpcInvokePermission.Anyone; // default value MUST be == `ServerRpcAttribute.RequireOwnership` + var invokePermission = RpcInvokePermission.Anyone; foreach (var attrField in rpcAttribute.Fields) { switch (attrField.Name) @@ -2873,6 +2873,9 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition case k_ServerRpcAttribute_RequireOwnership: invokePermission = (attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value) ? RpcInvokePermission.Owner : RpcInvokePermission.Anyone; break; + case k_RpcAttribute_InvokePermission: + invokePermission = (RpcInvokePermission)attrField.Argument.Value; + break; } } From 553f8055080b2373517e7f5763ef72f7872fca00 Mon Sep 17 00:00:00 2001 From: xmanning Date: Thu, 2 Oct 2025 01:28:12 -0400 Subject: [PATCH 05/27] Another missing switch case --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index a5449cdf40..2cd6e220a0 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -1440,12 +1440,14 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass callMethod = callMethod.MakeGeneric(genericTypes.ToArray()); } - RpcInvokePermission invokePermission = RpcInvokePermission.Anyone; - + var invokePermission = RpcInvokePermission.Anyone; foreach (var attrField in rpcAttribute.Fields) { switch (attrField.Name) { + case k_ServerRpcAttribute_RequireOwnership: + invokePermission = (attrField.Argument.Type == rpcHandler.Module.TypeSystem.Boolean && (bool)attrField.Argument.Value) ? RpcInvokePermission.Owner : RpcInvokePermission.Anyone; + break; case k_RpcAttribute_InvokePermission: invokePermission = (RpcInvokePermission)attrField.Argument.Value; break; From 82a80dbc5f725f32b144f9a846c7aea9714636eb Mon Sep 17 00:00:00 2001 From: xmanning Date: Thu, 2 Oct 2025 15:58:32 -0400 Subject: [PATCH 06/27] Restore and deprecate RequireOwnership in favor of RpcInvokePermission --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 27 +++++++++++++++++++ .../Runtime/Core/NetworkBehaviour.cs | 9 ++++--- .../Runtime/Messaging/RpcAttributes.cs | 15 +++++++++-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index 2cd6e220a0..1f019e4264 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -1441,6 +1441,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass } var invokePermission = RpcInvokePermission.Anyone; + foreach (var attrField in rpcAttribute.Fields) { switch (attrField.Name) @@ -1537,6 +1538,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass private CustomAttribute CheckAndGetRpcAttribute(MethodDefinition methodDefinition) { CustomAttribute rpcAttribute = null; + foreach (var customAttribute in methodDefinition.CustomAttributes) { var customAttributeType_FullName = customAttribute.AttributeType.FullName; @@ -1620,6 +1622,30 @@ private CustomAttribute CheckAndGetRpcAttribute(MethodDefinition methodDefinitio return null; } + + bool hasRequireOwnership = false, hasInvokePermission = false; + + foreach (var argument in rpcAttribute.Fields) + { + switch (argument.Name) + { + case k_ServerRpcAttribute_RequireOwnership: + hasRequireOwnership = true; + break; + case k_RpcAttribute_InvokePermission: + hasInvokePermission = true; + break; + default: + break; + } + } + + if (hasRequireOwnership && hasInvokePermission) + { + m_Diagnostics.AddError("Rpc attribute cannot declare both RequireOwnership and InvokePermission!"); + return null; + } + // Checks for IsSerializable are moved to later as the check is now done by dynamically seeing if any valid // serializer OR extension method exists for it. return rpcAttribute; @@ -2366,6 +2392,7 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA m_Diagnostics.AddError($"{nameof(RpcAttribute)} contains field {field} which is not present in {nameof(RpcAttribute.RpcAttributeParams)}."); } } + instructions.Add(processor.Create(OpCodes.Ldloc, rpcAttributeParamsIdx)); // defaultTarget diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index c3e599d541..8a3cb7d2b2 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -330,13 +330,14 @@ internal FastBufferWriter __beginSendRpc(uint rpcMethodId, RpcParams rpcParams, { throw new RpcException("The NetworkBehaviour must be spawned before calling this method."); } - if (attributeParams.InvokePermission == RpcInvokePermission.Owner && !IsOwner) - { - throw new RpcException("This RPC can only be sent by its owner."); - } else if (attributeParams.InvokePermission == RpcInvokePermission.Server && !IsServer) + else if (attributeParams.InvokePermission == RpcInvokePermission.Server && !IsServer) { throw new RpcException("This RPC can only be sent by the server."); } + else if ((attributeParams.RequireOwnership || attributeParams.InvokePermission == RpcInvokePermission.Owner) && !IsOwner) + { + throw new RpcException("This RPC can only be sent by its owner."); + } return new FastBufferWriter(k_RpcMessageDefaultSize, Allocator.Temp, k_RpcMessageMaximumSize); } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs index 1e9b8c634e..bd4af49b89 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs @@ -67,6 +67,8 @@ public struct RpcAttributeParams /// When true, allows the RPC target to be overridden at runtime /// public bool AllowTargetOverride; + + public bool RequireOwnership; } // Must match the fields in RemoteAttributeParams @@ -80,6 +82,15 @@ public struct RpcAttributeParams /// public RpcInvokePermission InvokePermission; + /// + /// When true, only the owner of the object can execute this RPC + /// + /// + /// Deprecated in favor of . + /// + [Obsolete] + public bool RequireOwnership; + /// /// When true, local execution of the RPC is deferred until the next network tick /// @@ -116,7 +127,7 @@ public class ServerRpcAttribute : RpcAttribute /// When true, only the owner of the NetworkObject can invoke this ServerRpc. /// This property overrides the base RpcAttribute.RequireOwnership. /// - public bool RequireOwnership; + public new bool RequireOwnership; /// /// Initializes a new instance of ServerRpcAttribute that targets the server @@ -139,7 +150,7 @@ public class ClientRpcAttribute : RpcAttribute /// public ClientRpcAttribute() : base(SendTo.NotServer) { - + InvokePermission = RpcInvokePermission.Server; } } } From d4f3fec22c9a5fc6fda77bab992a2cf542388bf8 Mon Sep 17 00:00:00 2001 From: xmanning Date: Thu, 2 Oct 2025 18:05:36 -0400 Subject: [PATCH 07/27] Only do permission validations on server context.SenderId will be the server for a client receiving the proxied message, so the checks will not be accurate. The checks are also unnecessary considering the checks have already been done by the server. --- .../Messaging/Messages/ProxyMessage.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs index 2b7346c062..d4b354f32f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs @@ -43,25 +43,26 @@ public unsafe void Handle(ref NetworkContext context) } var observers = networkObject.Observers; - var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(WrappedMessage.Metadata.NetworkBehaviourId); - - RpcInvokePermission permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][WrappedMessage.Metadata.NetworkRpcMethodId]; - bool hasPermission = permission switch + // Validate message if server + if (networkManager.IsServer) { - RpcInvokePermission.Anyone => true, - RpcInvokePermission.Server => context.SenderId == networkManager.LocalClientId, - RpcInvokePermission.Owner => context.SenderId == networkBehaviour.OwnerClientId, - _ => false, - }; + var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(WrappedMessage.Metadata.NetworkBehaviourId); - // Do not handle the message if the sender does not have permission to do so. - if (!hasPermission) - { - return; - } + RpcInvokePermission permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][WrappedMessage.Metadata.NetworkRpcMethodId]; + bool hasPermission = permission switch + { + RpcInvokePermission.Anyone => true, + RpcInvokePermission.Server => context.SenderId == networkManager.LocalClientId, + RpcInvokePermission.Owner => context.SenderId == networkBehaviour.OwnerClientId, + _ => false, + }; + + // Do not handle the message if the sender does not have permission to do so. + if (!hasPermission) + { + return; + } - if (networkManager.IsServer) - { WrappedMessage.SenderClientId = context.SenderId; } From 087a2f7415d1d570ddd824166a78d51ddd9a5ff9 Mon Sep 17 00:00:00 2001 From: xmanning Date: Tue, 7 Oct 2025 14:46:01 -0400 Subject: [PATCH 08/27] ClientRpc properly assigned InvokePermission Assigning ClientRpc.InvokePermission in the constructor was incorrect. Since we can check if it is a ClientRpc easily, I just do that in ILPP --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 17 +++++++++++++++-- .../Runtime/Messaging/RpcAttributes.cs | 1 - 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index 1f019e4264..dae04c5ab3 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -1440,6 +1440,8 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass callMethod = callMethod.MakeGeneric(genericTypes.ToArray()); } + var isClientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; + var invokePermission = RpcInvokePermission.Anyone; foreach (var attrField in rpcAttribute.Fields) @@ -1455,7 +1457,12 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass } } - // __registerRpc(RpcMethodId, HandleFunc, methodName); + if (isClientRpc) + { + invokePermission = RpcInvokePermission.Server; + } + + // __registerRpc(RpcMethodId, HandleFunc, invokePermission, methodName); instructions.Add(processor.Create(OpCodes.Ldarg_0)); instructions.Add(processor.Create(OpCodes.Ldc_I4, unchecked((int)rpcMethodId))); instructions.Add(processor.Create(OpCodes.Ldnull)); @@ -2892,7 +2899,7 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition var processor = rpcHandler.Body.GetILProcessor(); var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; - var isCientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; + var isClientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; var isGenericRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.RpcAttribute_FullName; var invokePermission = RpcInvokePermission.Anyone; foreach (var attrField in rpcAttribute.Fields) @@ -2908,6 +2915,12 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition } } + // legacy ClientRpc should always be RpcInvokePermission.Server + if (isClientRpc) + { + invokePermission = RpcInvokePermission.Server; + } + rpcHandler.Body.InitLocals = true; // NetworkManager networkManager; rpcHandler.Body.Variables.Add(new VariableDefinition(m_NetworkManager_TypeRef)); diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs index bd4af49b89..fffb8a831b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs @@ -150,7 +150,6 @@ public class ClientRpcAttribute : RpcAttribute /// public ClientRpcAttribute() : base(SendTo.NotServer) { - InvokePermission = RpcInvokePermission.Server; } } } From e868ea0e6fdbe384afc5a14cc39bfa2971336f43 Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 17 Oct 2025 14:02:28 -0400 Subject: [PATCH 09/27] Move test files --- com.unity.netcode.gameobjects/Tests/Runtime/Rpc.meta | 3 +++ .../Tests/Runtime/{ => Rpc}/RpcManyClientsTests.cs | 0 .../Tests/Runtime/{ => Rpc}/RpcManyClientsTests.cs.meta | 0 .../Tests/Runtime/{ => Rpc}/RpcQueueTests.cs | 0 .../Tests/Runtime/{ => Rpc}/RpcQueueTests.cs.meta | 0 .../Tests/Runtime/{ => Rpc}/RpcTests.cs | 0 .../Tests/Runtime/{ => Rpc}/RpcTests.cs.meta | 0 .../Tests/Runtime/{ => Rpc}/RpcTypeSerializationTests.cs | 0 .../Tests/Runtime/{ => Rpc}/RpcTypeSerializationTests.cs.meta | 0 .../Tests/Runtime/{ => Rpc}/UniversalRpcTests.cs | 2 +- .../Tests/Runtime/{ => Rpc}/UniversalRpcTests.cs.meta | 0 11 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Rpc.meta rename com.unity.netcode.gameobjects/Tests/Runtime/{ => Rpc}/RpcManyClientsTests.cs (100%) rename com.unity.netcode.gameobjects/Tests/Runtime/{ => Rpc}/RpcManyClientsTests.cs.meta (100%) rename com.unity.netcode.gameobjects/Tests/Runtime/{ => Rpc}/RpcQueueTests.cs (100%) rename com.unity.netcode.gameobjects/Tests/Runtime/{ => Rpc}/RpcQueueTests.cs.meta (100%) rename com.unity.netcode.gameobjects/Tests/Runtime/{ => Rpc}/RpcTests.cs (100%) rename com.unity.netcode.gameobjects/Tests/Runtime/{ => Rpc}/RpcTests.cs.meta (100%) rename com.unity.netcode.gameobjects/Tests/Runtime/{ => Rpc}/RpcTypeSerializationTests.cs (100%) rename com.unity.netcode.gameobjects/Tests/Runtime/{ => Rpc}/RpcTypeSerializationTests.cs.meta (100%) rename com.unity.netcode.gameobjects/Tests/Runtime/{ => Rpc}/UniversalRpcTests.cs (99%) rename com.unity.netcode.gameobjects/Tests/Runtime/{ => Rpc}/UniversalRpcTests.cs.meta (100%) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc.meta new file mode 100644 index 0000000000..765c779812 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c6bfc9360d1b4fe495c54c1f3004bb39 +timeCreated: 1760724018 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/RpcManyClientsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs similarity index 100% rename from com.unity.netcode.gameobjects/Tests/Runtime/RpcManyClientsTests.cs rename to com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/RpcManyClientsTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs.meta similarity index 100% rename from com.unity.netcode.gameobjects/Tests/Runtime/RpcManyClientsTests.cs.meta rename to com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/RpcQueueTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcQueueTests.cs similarity index 100% rename from com.unity.netcode.gameobjects/Tests/Runtime/RpcQueueTests.cs rename to com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcQueueTests.cs diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/RpcQueueTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcQueueTests.cs.meta similarity index 100% rename from com.unity.netcode.gameobjects/Tests/Runtime/RpcQueueTests.cs.meta rename to com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcQueueTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/RpcTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcTests.cs similarity index 100% rename from com.unity.netcode.gameobjects/Tests/Runtime/RpcTests.cs rename to com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcTests.cs diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/RpcTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcTests.cs.meta similarity index 100% rename from com.unity.netcode.gameobjects/Tests/Runtime/RpcTests.cs.meta rename to com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/RpcTypeSerializationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcTypeSerializationTests.cs similarity index 100% rename from com.unity.netcode.gameobjects/Tests/Runtime/RpcTypeSerializationTests.cs rename to com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcTypeSerializationTests.cs diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/RpcTypeSerializationTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcTypeSerializationTests.cs.meta similarity index 100% rename from com.unity.netcode.gameobjects/Tests/Runtime/RpcTypeSerializationTests.cs.meta rename to com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcTypeSerializationTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/UniversalRpcTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs similarity index 99% rename from com.unity.netcode.gameobjects/Tests/Runtime/UniversalRpcTests.cs rename to com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs index bccb9aae40..f70f511f1d 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/UniversalRpcTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs @@ -432,7 +432,7 @@ public void DefaultToNotAuthorityDeferLocalRpc(RpcParams rpcParams) { OnRpcReceived(); } - + // RPCs with RequireOwnership = true [Rpc(SendTo.Everyone, RequireOwnership = true)] diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/UniversalRpcTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs.meta similarity index 100% rename from com.unity.netcode.gameobjects/Tests/Runtime/UniversalRpcTests.cs.meta rename to com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs.meta From 271e53f2c8a5a286edc1cc1bfad1a93e2df3a6a8 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 12:51:25 -0400 Subject: [PATCH 10/27] Get logic working and add tests --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 109 +++--- .../Runtime/Core/NetworkBehaviour.cs | 8 +- .../Messaging/Messages/ProxyMessage.cs | 2 +- .../Runtime/Messaging/Messages/RpcMessages.cs | 50 +-- .../Runtime/Messaging/RpcAttributes.cs | 12 +- .../Runtime/Messaging/RpcParams.cs | 5 + .../Tests/Runtime/Rpc/RpcInvocationTests.cs | 357 ++++++++++++++++++ .../Runtime/Rpc/RpcInvocationTests.cs.meta | 3 + 8 files changed, 446 insertions(+), 100 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index 6dfaf90168..3e78d45578 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -1314,8 +1314,6 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass } var rpcHandlers = new List<(uint RpcMethodId, MethodDefinition RpcHandler, string RpcMethodName, CustomAttribute rpcAttribute)>(); - bool isEditorOrDevelopment = assemblyDefines.Contains("UNITY_EDITOR") || assemblyDefines.Contains("DEVELOPMENT_BUILD"); - foreach (var methodDefinition in typeDefinition.Methods) { var rpcAttribute = CheckAndGetRpcAttribute(methodDefinition); @@ -1436,16 +1434,18 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass callMethod = callMethod.MakeGeneric(genericTypes.ToArray()); } + var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; var isClientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; - var invokePermission = RpcInvokePermission.Anyone; + var invokePermission = isServerRpc ? RpcInvokePermission.Owner : RpcInvokePermission.Everyone; foreach (var attrField in rpcAttribute.Fields) { switch (attrField.Name) { case k_ServerRpcAttribute_RequireOwnership: - invokePermission = (attrField.Argument.Type == rpcHandler.Module.TypeSystem.Boolean && (bool)attrField.Argument.Value) ? RpcInvokePermission.Owner : RpcInvokePermission.Anyone; + var requireOwnership = attrField.Argument.Type == rpcHandler.Module.TypeSystem.Boolean && (bool)attrField.Argument.Value; + invokePermission = requireOwnership ? RpcInvokePermission.Owner : RpcInvokePermission.Everyone; break; case k_RpcAttribute_InvokePermission: invokePermission = (RpcInvokePermission)attrField.Argument.Value; @@ -1583,17 +1583,24 @@ private CustomAttribute CheckAndGetRpcAttribute(MethodDefinition methodDefinitio isValid = false; } - if (customAttributeType_FullName == CodeGenHelpers.RpcAttribute_FullName && - !methodDefinition.Name.EndsWith("Rpc", StringComparison.OrdinalIgnoreCase)) + if (customAttributeType_FullName == CodeGenHelpers.ClientRpcAttribute_FullName && + !methodDefinition.Name.EndsWith("ClientRpc", StringComparison.OrdinalIgnoreCase)) { - m_Diagnostics.AddError(methodDefinition, "Rpc method must end with 'Rpc' suffix!"); + m_Diagnostics.AddError(methodDefinition, "ClientRpc method must end with 'ClientRpc' suffix!"); isValid = false; } - if (customAttributeType_FullName == CodeGenHelpers.ClientRpcAttribute_FullName && - !methodDefinition.Name.EndsWith("ClientRpc", StringComparison.OrdinalIgnoreCase)) + if (customAttributeType_FullName == CodeGenHelpers.RpcAttribute_FullName && + !methodDefinition.Name.EndsWith("Rpc", StringComparison.OrdinalIgnoreCase)) { - m_Diagnostics.AddError(methodDefinition, "ClientRpc method must end with 'ClientRpc' suffix!"); + m_Diagnostics.AddError(methodDefinition, "Rpc method must end with 'Rpc' suffix!"); + + // Extra compiler information if a method was defined as a local function + if (methodDefinition.Name.Contains("Rpc|", StringComparison.OrdinalIgnoreCase) && methodDefinition.Name.StartsWith("g__", StringComparison.OrdinalIgnoreCase)) + { + m_Diagnostics.AddError(methodDefinition, $"{methodDefinition.Name} appears to be a local function. Local functions cannot be RPCs."); + } + isValid = false; } @@ -1638,8 +1645,6 @@ private CustomAttribute CheckAndGetRpcAttribute(MethodDefinition methodDefinitio case k_RpcAttribute_InvokePermission: hasInvokePermission = true; break; - default: - break; } } @@ -2206,13 +2211,30 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour_getNetworkManager_MethodRef)); instructions.Add(processor.Create(OpCodes.Stloc, netManLocIdx)); - // if (networkManager == null || !networkManager.IsListening) return; + // if (networkManager == null || !networkManager.IsListening) { ... return }; instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); instructions.Add(processor.Create(OpCodes.Brfalse, returnInstr)); instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); instructions.Add(processor.Create(OpCodes.Callvirt, m_NetworkManager_getIsListening_MethodRef)); instructions.Add(processor.Create(OpCodes.Brtrue, lastInstr)); + var logNextInstr = processor.Create(OpCodes.Nop); + + // if (LogLevel.Normal > networkManager.LogLevel) + instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); + instructions.Add(processor.Create(OpCodes.Ldfld, m_NetworkManager_LogLevel_FieldRef)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)LogLevel.Normal)); + instructions.Add(processor.Create(OpCodes.Cgt)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, 0)); + instructions.Add(processor.Create(OpCodes.Ceq)); + instructions.Add(processor.Create(OpCodes.Brfalse, logNextInstr)); + + // Debug.LogError(...); + instructions.Add(processor.Create(OpCodes.Ldstr, "Rpc methods can only be invoked after starting the NetworkManager!")); + instructions.Add(processor.Create(OpCodes.Call, m_Debug_LogError_MethodRef)); + + instructions.Add(logNextInstr); + instructions.Add(returnInstr); instructions.Add(lastInstr); } @@ -2341,7 +2363,7 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA instructions.Add(processor.Create(OpCodes.Ldloca, rpcAttributeParamsIdx)); instructions.Add(processor.Create(OpCodes.Initobj, m_AttributeParamsType_TypeRef)); - RpcAttribute.RpcAttributeParams dflt = default; + RpcAttribute.RpcAttributeParams defaultParameters = default; foreach (var field in rpcAttribute.Fields) { var found = false; @@ -2351,8 +2373,8 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA { found = true; var value = field.Argument.Value; - var paramField = dflt.GetType().GetField(attrField.Name); - if (value != paramField.GetValue(dflt)) + var paramField = defaultParameters.GetType().GetField(attrField.Name); + if (value != paramField.GetValue(defaultParameters)) { instructions.Add(processor.Create(OpCodes.Ldloca, rpcAttributeParamsIdx)); var type = value.GetType(); @@ -2458,11 +2480,11 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA { if (paramIndex != paramCount - 1) { - m_Diagnostics.AddError(methodDefinition, $"{nameof(RpcParams)} must be the last parameter in a ClientRpc."); + m_Diagnostics.AddError(methodDefinition, $"{methodDefinition.Name} is invalid. {nameof(RpcParams)} must be the last parameter in a ClientRpc."); } if (!isGenericRpc) { - m_Diagnostics.AddError($"Only Rpcs may accept {nameof(RpcParams)} as a parameter."); + m_Diagnostics.AddError($"{methodDefinition.Name} is invalid. Only Rpcs may accept {nameof(RpcParams)} as a parameter."); } continue; } @@ -2897,26 +2919,17 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; var isClientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; var isGenericRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.RpcAttribute_FullName; - var invokePermission = RpcInvokePermission.Anyone; + var requireOwnership = true; // default value MUST be == `ServerRpcAttribute.RequireOwnership` foreach (var attrField in rpcAttribute.Fields) { switch (attrField.Name) { case k_ServerRpcAttribute_RequireOwnership: - invokePermission = (attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value) ? RpcInvokePermission.Owner : RpcInvokePermission.Anyone; - break; - case k_RpcAttribute_InvokePermission: - invokePermission = (RpcInvokePermission)attrField.Argument.Value; + requireOwnership = attrField.Argument.Type == typeSystem.Boolean && (bool)attrField.Argument.Value; break; } } - // legacy ClientRpc should always be RpcInvokePermission.Server - if (isClientRpc) - { - invokePermission = RpcInvokePermission.Server; - } - rpcHandler.Body.InitLocals = true; // NetworkManager networkManager; rpcHandler.Body.Variables.Add(new VariableDefinition(m_NetworkManager_TypeRef)); @@ -2942,7 +2955,7 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition processor.Append(lastInstr); } - if (isServerRpc && invokePermission == RpcInvokePermission.Owner) + if (isServerRpc && requireOwnership) { var roReturnInstr = processor.Create(OpCodes.Ret); var roLastInstr = processor.Create(OpCodes.Nop); @@ -2976,42 +2989,6 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition processor.Append(logNextInstr); - processor.Append(roReturnInstr); - processor.Append(roLastInstr); - } else if (invokePermission == RpcInvokePermission.Server) - { - var roReturnInstr = processor.Create(OpCodes.Ret); - var roLastInstr = processor.Create(OpCodes.Nop); - - // if (rpcParams.Server.Receive.SenderClientId != NetworkManager.IsServer) { ... } return; - processor.Emit(OpCodes.Ldarg_2); - processor.Emit(OpCodes.Ldfld, m_RpcParams_Server_FieldRef); - processor.Emit(OpCodes.Ldfld, m_ServerRpcParams_Receive_FieldRef); - processor.Emit(OpCodes.Ldfld, m_ServerRpcParams_Receive_SenderClientId_FieldRef); - processor.Emit(OpCodes.Ldarg_0); - processor.Emit(OpCodes.Call, m_NetworkManager_getIsServer_MethodRef); - processor.Emit(OpCodes.Ceq); - processor.Emit(OpCodes.Ldc_I4, 0); - processor.Emit(OpCodes.Ceq); - processor.Emit(OpCodes.Brfalse, roLastInstr); - - var logNextInstr = processor.Create(OpCodes.Nop); - - // if (LogLevel.Normal > networkManager.LogLevel) - processor.Emit(OpCodes.Ldloc, netManLocIdx); - processor.Emit(OpCodes.Ldfld, m_NetworkManager_LogLevel_FieldRef); - processor.Emit(OpCodes.Ldc_I4, (int)LogLevel.Normal); - processor.Emit(OpCodes.Cgt); - processor.Emit(OpCodes.Ldc_I4, 0); - processor.Emit(OpCodes.Ceq); - processor.Emit(OpCodes.Brfalse, logNextInstr); - - // Debug.LogError(...); - processor.Emit(OpCodes.Ldstr, "Only the server can invoke an Rpc with RpcInvokePermission.Server!"); - processor.Emit(OpCodes.Call, m_Debug_LogError_MethodRef); - - processor.Append(logNextInstr); - processor.Append(roReturnInstr); processor.Append(roLastInstr); } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 1250924b69..43e42758fe 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -326,14 +326,16 @@ internal FastBufferWriter __beginSendRpc(uint rpcMethodId, RpcParams rpcParams, #pragma warning restore IDE1006 // restore naming rule violation check { if (m_NetworkObject == null && !IsSpawned) - { + { throw new RpcException("The NetworkBehaviour must be spawned before calling this method."); } - else if (attributeParams.InvokePermission == RpcInvokePermission.Server && !IsServer) + + if (attributeParams.InvokePermission == RpcInvokePermission.Server && !IsServer) { throw new RpcException("This RPC can only be sent by the server."); } - else if ((attributeParams.RequireOwnership || attributeParams.InvokePermission == RpcInvokePermission.Owner) && !IsOwner) + + if ((attributeParams.RequireOwnership || attributeParams.InvokePermission == RpcInvokePermission.Owner) && !IsOwner) { throw new RpcException("This RPC can only be sent by its owner."); } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs index d4b354f32f..5d2fa4843c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs @@ -51,7 +51,7 @@ public unsafe void Handle(ref NetworkContext context) RpcInvokePermission permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][WrappedMessage.Metadata.NetworkRpcMethodId]; bool hasPermission = permission switch { - RpcInvokePermission.Anyone => true, + RpcInvokePermission.Everyone => true, RpcInvokePermission.Server => context.SenderId == networkManager.LocalClientId, RpcInvokePermission.Owner => context.SenderId == networkBehaviour.OwnerClientId, _ => false, diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs index 2b74f13186..d0ce5f717b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs @@ -66,40 +66,37 @@ public static void Handle(ref NetworkContext context, ref RpcMetadata metadata, { NetworkLog.LogWarning($"[{metadata.NetworkObjectId}, {metadata.NetworkBehaviourId}, {metadata.NetworkRpcMethodId}] An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); } + + return; } - var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(metadata.NetworkBehaviourId); + var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(metadata.NetworkBehaviourId); try { - Type type = networkBehaviour.GetType(); - if (networkManager.IsServer) + var permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId]; + + if ((permission == RpcInvokePermission.Server && rpcParams.SenderId != NetworkManager.ServerClientId) || + (permission == RpcInvokePermission.Owner && rpcParams.SenderId != networkObject.OwnerClientId)) { - RpcInvokePermission permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId]; - bool hasPermission = permission switch - { - RpcInvokePermission.Anyone => true, - RpcInvokePermission.Server => context.SenderId == networkManager.LocalClientId, - RpcInvokePermission.Owner => context.SenderId == networkBehaviour.OwnerClientId, - _ => false, - }; - - // Do not handle the message if the sender does not have permission to do so. - if (!hasPermission) + if (networkManager.LogLevel <= LogLevel.Developer) { - return; + Debug.LogError("Rpc message received from a client who does not have permission to perform this operation!"); } - } + return; + } + NetworkBehaviour.__rpc_func_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId](networkBehaviour, payload, rpcParams); } catch (Exception ex) { - Debug.LogException(new Exception("Unhandled RPC exception!", ex)); - if (networkManager.LogLevel == LogLevel.Developer) + Debug.LogException(new Exception($"Unhandled RPC exception!", ex)); + if (networkManager.LogLevel <= LogLevel.Developer) { Debug.Log($"RPC Table Contents"); foreach (var entry in NetworkBehaviour.__rpc_func_table[networkBehaviour.GetType()]) { - Debug.Log($"{entry.Key} | {entry.Value.Method.Name}"); + var permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId]; + Debug.Log($"{entry.Key} | {entry.Value.Method.Name} | {permission}"); } } } @@ -138,6 +135,7 @@ public void Handle(ref NetworkContext context) { var rpcParams = new __RpcParams { + SenderId = context.SenderId, Server = new ServerRpcParams { Receive = new ServerRpcReceiveParams @@ -175,6 +173,7 @@ public void Handle(ref NetworkContext context) { var rpcParams = new __RpcParams { + SenderId = NetworkManager.ServerClientId, Client = new ClientRpcParams { Receive = new ClientRpcReceiveParams @@ -214,18 +213,19 @@ public unsafe bool Deserialize(FastBufferReader reader, ref NetworkContext conte public void Handle(ref NetworkContext context) { var networkManager = (NetworkManager)context.SystemOwner; - if (networkManager.IsServer) - { - SenderClientId = context.SenderId; - } - + + // If the server is receiving, always trust the transportId for the SenderClientId + // Otherwise, use the proxied id. + var senderId = networkManager.IsServer ? context.SenderId : SenderClientId; + var rpcParams = new __RpcParams { + SenderId = senderId, Ext = new RpcParams { Receive = new RpcReceiveParams { - SenderClientId = SenderClientId + SenderClientId = senderId } } }; diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs index fffb8a831b..be8c6683c9 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs @@ -24,13 +24,15 @@ public enum RpcDelivery public enum RpcInvokePermission { /// - /// Anyone can invoke the Rpc. + /// Any connected client can invoke the Rpc. /// - Anyone = 0, + Everyone = 0, + /// /// Rpc can only be invoked by the server. /// Server, + /// /// Rpc can only be invoked by the owner of the NetworkBehaviour. /// @@ -86,9 +88,9 @@ public struct RpcAttributeParams /// When true, only the owner of the object can execute this RPC /// /// - /// Deprecated in favor of . + /// Deprecated in favor of . /// - [Obsolete] + [Obsolete("RequireOwnership is deprecated. Please use InvokePermission = RpcInvokePermission.Owner instead.")] public bool RequireOwnership; /// @@ -125,7 +127,7 @@ public class ServerRpcAttribute : RpcAttribute { /// /// When true, only the owner of the NetworkObject can invoke this ServerRpc. - /// This property overrides the base RpcAttribute.RequireOwnership. + /// This property sets the base to when "RequireOwnership = true" or to when "RequireOwnership = false". />. /// public new bool RequireOwnership; diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcParams.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcParams.cs index b302e3f8f7..193dc64d2d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcParams.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcParams.cs @@ -211,5 +211,10 @@ internal struct __RpcParams public RpcParams Ext; public ServerRpcParams Server; public ClientRpcParams Client; + + /// + /// Internal information used by to help handle this message. + /// + internal ulong SenderId; } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs new file mode 100644 index 0000000000..739f8fd126 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using NUnit.Framework; +using Unity.Collections; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests.Rpc +{ + [TestFixture(NetworkTopologyTypes.DistributedAuthority)] + [TestFixture(NetworkTopologyTypes.ClientServer)] + internal class RpcInvocationTests: NetcodeIntegrationTest + { + protected override int NumberOfClients => 3; + + public RpcInvocationTests(NetworkTopologyTypes topologyType) : base(topologyType) {} + + private GameObject m_Prefab; + + private Dictionary m_InvokeInstances = new(); + + protected override void OnServerAndClientsCreated() + { + m_Prefab = CreateNetworkObjectPrefab("RpcInvokePermissionTest"); + m_Prefab.AddComponent(); + + base.OnServerAndClientsCreated(); + } + + private void BuildInvokeInstancesMap(ulong networkObjectId) + { + m_InvokeInstances.Clear(); + foreach (var manager in m_NetworkManagers) + { + Assert.IsTrue(manager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out var instance)); + m_InvokeInstances.Add(manager, instance.GetComponent()); + } + } + + private bool AllExpectedCallsReceived(StringBuilder errorLog) + { + var allInstancesValid = true; + foreach (var instance in m_InvokeInstances.Values) + { + if (!instance.HasReceivedExpectedRpcs(errorLog)) + { + allInstancesValid = false; + } + } + return allInstancesValid; + } + + [UnityTest] + public IEnumerator RpcInvokePermissionSendingTests() + { + var nonAuthority = GetNonAuthorityNetworkManager(); + + var authorityInstance = SpawnObject(m_Prefab, nonAuthority).GetComponent(); + + yield return WaitForSpawnedOnAllOrTimeOut(authorityInstance); + AssertOnTimeout("Failed to spawn InvokePermissions test object"); + + BuildInvokeInstancesMap(authorityInstance.NetworkObjectId); + + // [Rpc(SendTo.Everyone, InvokePermission.Server)] + foreach (var (manager, instance) in m_InvokeInstances) + { + // When using the Cmb service there is no server so no calls should be made. + if (!m_UseCmbService) + { + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.ServerInvokePermissionRpc)] = 1; + } + + var threwException = false; + try + { + instance.ServerInvokePermissionRpc(); + } + catch (RpcException) + { + Assert.IsFalse(manager.IsServer); + threwException = true; + } + + if (!manager.IsServer) + { + Assert.IsTrue(threwException); + } + } + + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout("[InvokePermissions.Server] Rpc invoked an incorrect number of times"); + + // [Rpc(SendTo.Everyone, InvokePermission.Owner)] + foreach (var (_, instance) in m_InvokeInstances) + { + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.OwnerInvokePermissionRpc)] = 1; + + var threwException = false; + try + { + instance.OwnerInvokePermissionRpc(); + } + catch (RpcException) + { + Assert.IsFalse(instance.IsOwner); + threwException = true; + } + + if (!instance.IsOwner) + { + Assert.IsTrue(threwException); + } + } + + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout("[InvokePermissions.Owner] Rpc invoked an incorrect number of times"); + + // [Rpc(SendTo.Everyone, InvokePermission.Everyone)] + foreach (var (_, instance) in m_InvokeInstances) + { + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.EveryoneInvokePermissionRpc)] = NumberOfClients + 1; + + try + { + instance.EveryoneInvokePermissionRpc(); + } + catch (RpcException e) + { + Assert.Fail($"Unexpected RpcException was thrown! Exception: {e}"); + } + } + + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout("[InvokePermissions.Everyone] Rpc invoked an incorrect number of times"); + } + + + [UnityTest] + public IEnumerator RpcInvokePermissionReceivingTests() + { + var firstClient = GetNonAuthorityNetworkManager(0); + + var spawnedObject = SpawnObject(m_Prefab, firstClient).GetComponent(); + + yield return WaitForSpawnedOnAllOrTimeOut(spawnedObject); + AssertOnTimeout("Failed to spawn InvokePermissions test object"); + + BuildInvokeInstancesMap(spawnedObject.NetworkObjectId); + + // [Rpc(SendTo.Everyone, InvokePermission.Server)] + foreach (var (manager, instance) in m_InvokeInstances) + { + // When using the Cmb service there is no server so no calls should be made. + if (!m_UseCmbService) + { + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.ServerInvokePermissionRpc)] = 1; + } + + SendUncheckedMessage(manager, instance, nameof(InvokePermissionBehaviour.ServerInvokePermissionRpc)); + } + + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout("[InvokePermissions.Server] Incorrect Rpc calls received"); + + // [Rpc(SendTo.Everyone, InvokePermission.Owner)] + foreach (var (manager, instance) in m_InvokeInstances) + { + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.OwnerInvokePermissionRpc)] = 1; + + SendUncheckedMessage(manager, instance, nameof(InvokePermissionBehaviour.OwnerInvokePermissionRpc)); + } + + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout("[InvokePermissions.Owner] Incorrect Rpc calls received"); + + // [Rpc(SendTo.Everyone, InvokePermission.Everyone)] + foreach (var (manager, instance) in m_InvokeInstances) + { + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.EveryoneInvokePermissionRpc)] = NumberOfClients + 1; + + SendUncheckedMessage(manager, instance, nameof(InvokePermissionBehaviour.EveryoneInvokePermissionRpc)); + } + + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout("[InvokePermissions.Everyone] Incorrect Rpc calls received"); + + // DANGO-TODO: Fix the client spoofing issue + if (!m_UseCmbService) + { + var firstClientInstance = m_InvokeInstances[firstClient]; + var secondClient = GetNonAuthorityNetworkManager(1); + var thirdClient = GetNonAuthorityNetworkManager(2); + + firstClientInstance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.TrackSenderIdRpc)] = 1; + + // Manually set the senderId to an incorrect value + var secondClientInstance = m_InvokeInstances[secondClient]; + var bufferWriter = new FastBufferWriter(1024, Allocator.Temp); + using (bufferWriter) + { + var rpcMessage = new RpcMessage + { + Metadata = new RpcMetadata + { + NetworkObjectId = secondClientInstance.NetworkObjectId, + NetworkBehaviourId = secondClientInstance.NetworkBehaviourId, + NetworkRpcMethodId = GetMethodIdFromMethodName(nameof(InvokePermissionBehaviour.TrackSenderIdRpc)), + }, + // Set the sender to the third client + SenderClientId = thirdClient.LocalClientId, + WriteBuffer = bufferWriter + }; + + // Send the message on the second client + secondClientInstance.RpcTarget.Owner.Send(secondClientInstance, ref rpcMessage, NetworkDelivery.Reliable, new RpcParams()); + } + + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout("[SpoofedSenderId] Incorrect Rpc calls received"); + + Assert.That(firstClientInstance.SenderIdReceived, Is.EqualTo(secondClient.LocalClientId), "Received spoofed sender id!"); + } + } + + private void SendUncheckedMessage(NetworkManager manager, InvokePermissionBehaviour invokePermissionsObject, string rpcMethodName) + { + using var bufferWriter = new FastBufferWriter(1024, Allocator.Temp); + var rpcMessage = new RpcMessage + { + Metadata = new RpcMetadata + { + NetworkObjectId = invokePermissionsObject.NetworkObjectId, + NetworkBehaviourId = invokePermissionsObject.NetworkBehaviourId, + NetworkRpcMethodId = GetMethodIdFromMethodName(rpcMethodName), + }, + SenderClientId = manager.LocalClientId, + WriteBuffer = bufferWriter + }; + + invokePermissionsObject.RpcTarget.Everyone.Send(invokePermissionsObject, ref rpcMessage, NetworkDelivery.Reliable, new RpcParams()); + } + + private static readonly Dictionary k_MethodIdLookups = new(); + + private uint GetMethodIdFromMethodName(string methodName) + { + if (k_MethodIdLookups.TryGetValue(methodName, out var id)) + { + return id; + } + + var nameLookup = NetworkBehaviour.__rpc_name_table.GetValueOrDefault(typeof(InvokePermissionBehaviour)); + + foreach (var (rpcMethodId, rpcMethodName) in nameLookup) + { + if (rpcMethodName == methodName) + { + k_MethodIdLookups.Add(rpcMethodName, rpcMethodId); + return rpcMethodId; + } + } + + Assert.Fail($"Method \"{methodName}\" was not found in rpc method id lookups."); + return default; + } + } + + internal class InvokePermissionBehaviour : NetworkBehaviour + { + public readonly Dictionary RpcCallCounts = new(); + public readonly Dictionary ExpectedCallCounts = new(); + + public bool HasReceivedExpectedRpcs(StringBuilder errorLog) + { + var isValid = true; + var seen = new HashSet(); + foreach (var (expectedMethodCall, expectedCallCount) in ExpectedCallCounts) + { + seen.Add(expectedMethodCall); + if (!RpcCallCounts.TryGetValue(expectedMethodCall, out var actualCallCount)) + { + errorLog.AppendLine($"[Client-{NetworkManager.LocalClientId}] Expected {expectedMethodCall} to have been invoked!"); + } + + if (expectedCallCount != actualCallCount) + { + isValid = false; + errorLog.AppendLine($"[Client-{NetworkManager.LocalClientId}] {expectedMethodCall} was invoked an incorrect number of times! Expected: {expectedCallCount}, Received: {actualCallCount}"); + } + } + + // Ensure no other rpcs were called when they weren't expected + foreach (var rpcCall in RpcCallCounts.Keys) + { + if (!seen.Contains(rpcCall)) + { + isValid = false; + errorLog.AppendLine($"[Client-{NetworkManager.LocalClientId}] {rpcCall} was invoked when it should not have been."); + } + } + + return isValid; + } + + public void Reset() + { + RpcCallCounts.Clear(); + ExpectedCallCounts.Clear(); + } + + [Rpc(SendTo.Everyone, InvokePermission = RpcInvokePermission.Server)] + public void ServerInvokePermissionRpc() + { + TrackRpcCalled(GetCaller()); + } + + [Rpc(SendTo.Everyone, InvokePermission = RpcInvokePermission.Owner)] + public void OwnerInvokePermissionRpc() + { + TrackRpcCalled(GetCaller()); + } + + [Rpc(SendTo.Everyone, InvokePermission = RpcInvokePermission.Everyone)] + public void EveryoneInvokePermissionRpc() + { + TrackRpcCalled(GetCaller()); + } + + internal ulong SenderIdReceived; + [Rpc(SendTo.Owner)] + public void TrackSenderIdRpc(RpcParams rpcParams) + { + TrackRpcCalled(GetCaller()); + SenderIdReceived = rpcParams.Receive.SenderClientId; + } + + private void TrackRpcCalled(string rpcName) + { + // TryAdd returns false and will not add anything if the key already existed. + if (!RpcCallCounts.TryAdd(rpcName, 1)) + { + // If the key already existed, increment it + RpcCallCounts[rpcName]++; + } + } + + private static string GetCaller([CallerMemberName] string caller = null) + { + return caller; + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs.meta new file mode 100644 index 0000000000..5337d29a7b --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9833d655491744a199494f0715dc8a62 +timeCreated: 1760724189 \ No newline at end of file From 89ea413191ab0983ae8a707f682e9151feb9d9da Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 12:52:20 -0400 Subject: [PATCH 11/27] Mark ServerRpc.RequireOwnership as deprecated --- .../advanced-topics/message-system/rpc.md | 2 +- .../Documentation~/basics/networkvariable.md | 2 +- .../terms-concepts/ownership.md | 4 +- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 5 +++ .../Runtime/Messaging/RpcAttributes.cs | 16 ++++++++ .../Editor/NetworkBehaviourEditorTests.cs | 7 ++++ .../NetworkTransformParentingTests.cs | 4 +- .../Tests/Runtime/Rpc/RpcManyClientsTests.cs | 20 +++++----- .../Tests/Runtime/Rpc/UniversalRpcTests.cs | 26 ++++++------- .../Samples/SpawnObject/SpawnObjectHandler.cs | 8 ++-- testproject/Assets/Scripts/GrabbableBall.cs | 4 +- .../LinearMotionHandler.cs | 2 +- .../HybridScripts/RpcQueueManualTests.cs | 14 +++---- .../ChildObjectScript.cs | 8 ++-- .../LerpVsSlerpControls.cs | 4 +- .../AnimatedCubeController.cs | 5 +-- .../Scripts/IntegrationNetworkTransform.cs | 6 +-- .../Tests/Manual/Scripts/StatsDisplay.cs | 2 +- .../Assets/Tests/Runtime/RpcObserverTests.cs | 4 +- .../Runtime/RpcUserSerializableTypesTest.cs | 38 +++++++++---------- .../Runtime/ServerDisconnectsClientTest.cs | 2 +- .../ShutdownDuringOnNetworkSpawnBehaviour.cs | 2 +- .../MultiprocessRuntime/TestCoordinator.cs | 9 ++--- 23 files changed, 110 insertions(+), 84 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md index c004571dbd..838ad3f5d6 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md @@ -204,7 +204,7 @@ There are a few other parameters that can be passed to either the `Rpc` attribut | Parameter | Description | | ----------------------- | ------------------------------------------------------------ | | `Delivery` | Controls whether the delivery is reliable (default) or unreliable.

Options: `RpcDelivery.Reliable` or `RpcDelivery.Unreliable`
Default: `RpcDelivery.Reliable` | -| `RequireOwnership` | If `true`, this RPC throws an exception if invoked by a player that does not own the object. This is in effect for server-to-client, client-to-server, and client-to-client RPCs - i.e., a server-to-client RPC will still fail if the server is not the object's owner.

Default: `false` | +| `InvokePermission` | Controls how an RPC is allowed to be invoked.

Options:
`RpcInvokePermission.Server` - This RPC throws an exception if invoked by a game client that is not the server.
`RpcInvokePermission.Owner` - This RPC throws an exception if invoked by a game client that does not own the object.
`RpcInvokePermission.Everyone` - This can be invoked by any connected game client.
Default: `RpcInvokePermission.Everyone` | | `DeferLocal` | If `true`, RPCs that execute locally will be deferred until the start of the next frame, as if they had been sent over the network. (They will not actually be sent over the network, but will be treated as if they were.) This is useful for mutually recursive RPCs on hosts, where sending back and forth between the server and the "host client" will cause a stack overflow if each RPC is executed instantly; simulating the flow of RPCs between remote client and server enables this flow to work the same in both contexts.

Default: `false` | | `AllowTargetOverride` | By default, any `SendTo` value other than `SendTo.SpecifiedInParams` is a hard-coded value that cannot be changed. Setting this to `true` allows you to provide an alternate target at runtime, while using the `SendTo` value as a fallback if no runtime value is provided. | diff --git a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md index ab648a7056..d5bd4cf875 100644 --- a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md +++ b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md @@ -145,7 +145,7 @@ This works the same way with dynamically spawned NetworkObjects. The [synchronization and notification example](#synchronization-and-notification-example) highlights the differences between synchronizing a `NetworkVariable` with newly-joining clients and notifying connected clients when a `NetworkVariable` changes, but it doesn't provide any concrete example usage. -The `OnValueChanged` example shows a simple server-authoritative `NetworkVariable` being used to track the state of a door (that is, open or closed) using a non-ownership-based server RPC. With `RequireOwnership = false` any client can notify the server that it's performing an action on the door. Each time the door is used by a client, the `Door.ToggleServerRpc` is invoked and the server-side toggles the state of the door. When the `Door.State.Value` changes, all connected clients are synchronized to the (new) current `Value` and the `OnStateChanged` method is invoked locally on each client. +The `OnValueChanged` example shows a simple server-authoritative `NetworkVariable` being used to track the state of a door (that is, open or closed) using an RPC that is sent to the server. Each time the door is used by a client, the `Door.ToggleServerRpc` is invoked and the server-side toggles the state of the door. When the `Door.State.Value` changes, all connected clients are synchronized to the (new) current `Value` and the `OnStateChanged` method is invoked locally on each client. ```csharp public class Door : NetworkBehaviour diff --git a/com.unity.netcode.gameobjects/Documentation~/terms-concepts/ownership.md b/com.unity.netcode.gameobjects/Documentation~/terms-concepts/ownership.md index ab536a17cd..1d0427c1ce 100644 --- a/com.unity.netcode.gameobjects/Documentation~/terms-concepts/ownership.md +++ b/com.unity.netcode.gameobjects/Documentation~/terms-concepts/ownership.md @@ -8,7 +8,7 @@ Netcode for GameObjects also supports building games with a [distributed authori ## Ownership in client-server -In a client-server topology, the server has ultimate authority over all NetworkObjects. Clients can request ownership of [specific objects](../components/core/networkobject.md#ownership), but the server has the final say in whether to grant or deny these requests. +In a client-server topology, the server has ultimate authority over all NetworkObjects. Clients can request ownership of [specific objects](../components/core/networkobject.md#ownership), but the server has the final say in whether to grant or deny these requests.x ## Ownership in distributed authority @@ -58,4 +58,4 @@ When requesting ownership of a NetworkObject using [`NetworkObject.RequestOwners * [Authority](authority.md) * [Client-server](client-server.md) -* [Distributed authority](distributed-authority.md) \ No newline at end of file +* [Distributed authority](distributed-authority.md) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index 3e78d45578..f08e5783cd 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -610,7 +610,12 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly, private const string k_RpcAttribute_Delivery = nameof(RpcAttribute.Delivery); private const string k_RpcAttribute_InvokePermission = nameof(RpcAttribute.InvokePermission); + +#pragma warning disable CS0618 // Type or member is obsolete +// Need to ignore the obsolete warning as the obsolete behaviour still needs to work private const string k_ServerRpcAttribute_RequireOwnership = nameof(ServerRpcAttribute.RequireOwnership); +#pragma warning restore CS0618 // Type or member is obsolete + private const string k_RpcParams_Server = nameof(__RpcParams.Server); private const string k_RpcParams_Client = nameof(__RpcParams.Client); private const string k_RpcParams_Ext = nameof(__RpcParams.Ext); diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs index be8c6683c9..1fbfa5979d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs @@ -129,6 +129,22 @@ public class ServerRpcAttribute : RpcAttribute /// When true, only the owner of the NetworkObject can invoke this ServerRpc. /// This property sets the base to when "RequireOwnership = true" or to when "RequireOwnership = false". />. ///
+ /// + /// Deprecated in favor of using with and an . + /// + /// [ServerRpc(RequireOwnership = false)] + /// // is replaced with + /// [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] + /// // or, as InvokePermission has a default setting of RpcInvokePermission.Everyone, you can also use + /// [Rpc(SendTo.Server)] + /// + /// + /// [ServerRpc(RequireOwnership = true)] + /// // is replaced with + /// [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] + /// + /// + [Obsolete("ServerRpc with RequireOwnership is deprecated. Use [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] or [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] instead.)]")] public new bool RequireOwnership; /// diff --git a/com.unity.netcode.gameobjects/Tests/Editor/NetworkBehaviourEditorTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/NetworkBehaviourEditorTests.cs index 67bc886ae7..97e7343131 100644 --- a/com.unity.netcode.gameobjects/Tests/Editor/NetworkBehaviourEditorTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Editor/NetworkBehaviourEditorTests.cs @@ -35,6 +35,8 @@ public void AccessNetworkObjectTest() var gameObject = new GameObject(nameof(AccessNetworkObjectTest)); var networkBehaviour = gameObject.AddComponent(); + networkBehaviour.NoNetworkRpc(); + Assert.That(networkBehaviour.NetworkObject, Is.Null); var networkObject = gameObject.AddComponent(); @@ -77,6 +79,11 @@ internal class DerivedNetworkBehaviour : EmptyNetworkBehaviour internal class EmptyNetworkBehaviour : NetworkBehaviour { + [Rpc(SendTo.Everyone)] + public void NoNetworkRpc() + { + Debug.Log("No Network Rpc"); + } } } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformParentingTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformParentingTests.cs index 896e7ddda2..c76e7b3ef8 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformParentingTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformParentingTests.cs @@ -99,8 +99,8 @@ public override void OnNetworkSpawn() /// A ServerRpc that requests the server to spawn a player object for the client that invoked this RPC. /// /// Parameters for the ServerRpc, including the sender's client ID. - [ServerRpc(RequireOwnership = false)] - private void RequestPlayerObjectSpawnServerRpc(ServerRpcParams rpcParams = default) + [Rpc(SendTo.Server)] + private void RequestPlayerObjectSpawnServerRpc(RpcParams rpcParams = default) { SpawnedPlayer = Instantiate(PlayerPrefab); SpawnedPlayer.SpawnAsPlayerObject(rpcParams.Receive.SenderClientId); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs index 5af6daa6c0..502715dba9 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs @@ -11,33 +11,33 @@ internal class RpcManyClientsObject : NetworkBehaviour { public int Count = 0; public List ReceivedFrom = new List(); - [ServerRpc(RequireOwnership = false)] - public void ResponseServerRpc(ServerRpcParams rpcParams = default) + [Rpc(SendTo.Server)] + public void ResponseServerRpc(RpcParams rpcParams = default) { ReceivedFrom.Add(rpcParams.Receive.SenderClientId); Count++; } - [ClientRpc] + [Rpc(SendTo.ClientsAndHost)] public void NoParamsClientRpc() { ResponseServerRpc(); } - [ClientRpc] + [Rpc(SendTo.ClientsAndHost)] public void OneParamClientRpc(int value) { ResponseServerRpc(); } - [ClientRpc] + [Rpc(SendTo.ClientsAndHost)] public void TwoParamsClientRpc(int value1, int value2) { ResponseServerRpc(); } - [ClientRpc] - public void WithParamsClientRpc(ClientRpcParams param) + [Rpc(SendTo.SpecifiedInParams)] + public void WithParamsClientRpc(RpcParams param) { ResponseServerRpc(); } @@ -114,7 +114,7 @@ public void RpcManyClientsTest() success = WaitForConditionOrTimeOutWithTimeTravel(() => TotalClients == rpcManyClientsObject.Count); Assert.True(success, $"Timed out wait for {nameof(rpcManyClientsObject.OneParamClientRpc)}! Only {rpcManyClientsObject.Count} of {TotalClients} was received!"); - var param = new ClientRpcParams(); + var param = new RpcParams(); rpcManyClientsObject.Count = 0; rpcManyClientsObject.TwoParamsClientRpc(0, 0); // RPC with two params @@ -127,8 +127,8 @@ public void RpcManyClientsTest() rpcManyClientsObject.ReceivedFrom.Clear(); rpcManyClientsObject.Count = 0; - var target = new List { m_ClientNetworkManagers[1].LocalClientId, m_ClientNetworkManagers[2].LocalClientId }; - param.Send.TargetClientIds = target; + var target = new [] { m_ClientNetworkManagers[1].LocalClientId, m_ClientNetworkManagers[2].LocalClientId }; + param.Send.Target = rpcManyClientsObject.RpcTarget.Group(target, RpcTargetUse.Temp); rpcManyClientsObject.WithParamsClientRpc(param); messageHookList.Clear(); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs index f70f511f1d..be1a15de20 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs @@ -432,70 +432,70 @@ public void DefaultToNotAuthorityDeferLocalRpc(RpcParams rpcParams) { OnRpcReceived(); } - - // RPCs with RequireOwnership = true - [Rpc(SendTo.Everyone, RequireOwnership = true)] + // RPCs with InvokePermission.Owner + + [Rpc(SendTo.Everyone, InvokePermission = RpcInvokePermission.Owner)] public void DefaultToEveryoneRequireOwnershipRpc() { OnRpcReceived(); } - [Rpc(SendTo.Me, RequireOwnership = true)] + [Rpc(SendTo.Me, InvokePermission = RpcInvokePermission.Owner)] public void DefaultToMeRequireOwnershipRpc() { OnRpcReceived(); } - [Rpc(SendTo.Owner, RequireOwnership = true)] + [Rpc(SendTo.Owner, InvokePermission = RpcInvokePermission.Owner)] public void DefaultToOwnerRequireOwnershipRpc() { OnRpcReceived(); } - [Rpc(SendTo.NotOwner, RequireOwnership = true)] + [Rpc(SendTo.NotOwner, InvokePermission = RpcInvokePermission.Owner)] public void DefaultToNotOwnerRequireOwnershipRpc() { OnRpcReceived(); } - [Rpc(SendTo.Server, RequireOwnership = true)] + [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] public void DefaultToServerRequireOwnershipRpc() { OnRpcReceived(); } - [Rpc(SendTo.NotMe, RequireOwnership = true)] + [Rpc(SendTo.NotMe, InvokePermission = RpcInvokePermission.Owner)] public void DefaultToNotMeRequireOwnershipRpc() { OnRpcReceived(); } - [Rpc(SendTo.NotServer, RequireOwnership = true)] + [Rpc(SendTo.NotServer, InvokePermission = RpcInvokePermission.Owner)] public void DefaultToNotServerRequireOwnershipRpc() { OnRpcReceived(); } - [Rpc(SendTo.ClientsAndHost, RequireOwnership = true)] + [Rpc(SendTo.ClientsAndHost, InvokePermission = RpcInvokePermission.Owner)] public void DefaultToClientsAndHostRequireOwnershipRpc() { OnRpcReceived(); } - [Rpc(SendTo.SpecifiedInParams, RequireOwnership = true)] + [Rpc(SendTo.SpecifiedInParams, InvokePermission = RpcInvokePermission.Owner)] public void SpecifiedInParamsRequireOwnershipRpc(RpcParams rpcParams) { OnRpcReceived(); } - [Rpc(SendTo.Authority, RequireOwnership = true)] + [Rpc(SendTo.Authority, InvokePermission = RpcInvokePermission.Owner)] public void DefaultToAuthorityRequireOwnershipRpc() { OnRpcReceived(); } - [Rpc(SendTo.NotAuthority, RequireOwnership = true)] + [Rpc(SendTo.NotAuthority, InvokePermission = RpcInvokePermission.Owner)] public void DefaultToNotAuthorityRequireOwnershipRpc() { OnRpcReceived(); diff --git a/testproject/Assets/Samples/SpawnObject/SpawnObjectHandler.cs b/testproject/Assets/Samples/SpawnObject/SpawnObjectHandler.cs index cf1ee9ee24..1a5b88b07b 100644 --- a/testproject/Assets/Samples/SpawnObject/SpawnObjectHandler.cs +++ b/testproject/Assets/Samples/SpawnObject/SpawnObjectHandler.cs @@ -65,8 +65,8 @@ private void AutoSpawnObject(ulong ownerId, int selection) SetMotion(networkObject.gameObject); } - [ServerRpc(RequireOwnership = false)] - private void AutoSpawnObjectServerRpc(int selection, ServerRpcParams serverRpcParams = default) + [Rpc(SendTo.Server)] + private void AutoSpawnObjectServerRpc(int selection, RpcParams serverRpcParams = default) { AutoSpawnObject(serverRpcParams.Receive.SenderClientId, selection); } @@ -88,8 +88,8 @@ public void AutoSpawnClick() } } - [ServerRpc(RequireOwnership = false)] - private void ManualSpawnObjectServerRpc(int selection, ServerRpcParams serverRpcParams = default) + [Rpc(SendTo.Server)] + private void ManualSpawnObjectServerRpc(int selection, RpcParams serverRpcParams = default) { AutoSpawnObject(serverRpcParams.Receive.SenderClientId, selection); } diff --git a/testproject/Assets/Scripts/GrabbableBall.cs b/testproject/Assets/Scripts/GrabbableBall.cs index 181338d455..89a7526937 100644 --- a/testproject/Assets/Scripts/GrabbableBall.cs +++ b/testproject/Assets/Scripts/GrabbableBall.cs @@ -75,8 +75,8 @@ public override void OnNetworkObjectParentChanged(NetworkObject parentNetworkObj } } - [ServerRpc(RequireOwnership = false)] - private void TryGrabServerRpc(ServerRpcParams serverRpcParams = default) + [Rpc(SendTo.Server)] + private void TryGrabServerRpc(RpcParams serverRpcParams = default) { if (!m_IsGrabbed.Value) { diff --git a/testproject/Assets/Tests/Manual/DeltaPositionNetworkTransform/LinearMotionHandler.cs b/testproject/Assets/Tests/Manual/DeltaPositionNetworkTransform/LinearMotionHandler.cs index d9b1567093..0e5ea3cc56 100644 --- a/testproject/Assets/Tests/Manual/DeltaPositionNetworkTransform/LinearMotionHandler.cs +++ b/testproject/Assets/Tests/Manual/DeltaPositionNetworkTransform/LinearMotionHandler.cs @@ -412,7 +412,7 @@ private void SetPositionText() ClientDelta.text = $"C-Delta: {GetVector3AsString(ref m_ClientDelta)}"; } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void OnNonAuthorityUpdatePositionServerRpc(Vector3 position) { m_ClientPosition = position; diff --git a/testproject/Assets/Tests/Manual/HybridScripts/RpcQueueManualTests.cs b/testproject/Assets/Tests/Manual/HybridScripts/RpcQueueManualTests.cs index db94d86579..ca7def99da 100644 --- a/testproject/Assets/Tests/Manual/HybridScripts/RpcQueueManualTests.cs +++ b/testproject/Assets/Tests/Manual/HybridScripts/RpcQueueManualTests.cs @@ -101,7 +101,7 @@ private enum NetworkManagerMode private ClientRpcDirectTestingModes m_ClientRpcDirectTestingMode; - private ServerRpcParams m_ServerRpcParams; + private RpcParams m_ServerRpcParams; private ClientRpcParams m_ClientRpcParams; private ClientRpcParams m_ClientRpcParamsMultiParameter; @@ -616,8 +616,8 @@ private void UnifiedDirectUpdate() /// /// the client side counter /// - [ServerRpc(RequireOwnership = false)] - private void OnSendCounterServerRpc(int counter, ulong clientId, ServerRpcParams parameters = default) + [Rpc(SendTo.Server)] + private void OnSendCounterServerRpc(int counter, ulong clientId, RpcParams parameters = default) { //This is just for debug purposes so I can trap for "non-local" clients if (m_ClientSpecificCounters.ContainsKey(parameters.Receive.SenderClientId)) @@ -638,8 +638,8 @@ private void OnSendCounterServerRpc(int counter, ulong clientId, ServerRpcParams /// Sends no parameters to the server /// /// - [ServerRpc(RequireOwnership = false)] - private void OnSendNoParametersServerRpc(ServerRpcParams parameters = default) + [Rpc(SendTo.Server)] + private void OnSendNoParametersServerRpc(RpcParams parameters = default) { m_ClientRpcParamsMultiParameter.Send.TargetClientIds = new[] { parameters.Receive.SenderClientId }; OnSendNoParametersClientRpc(m_ClientRpcParamsMultiParameter); @@ -650,8 +650,8 @@ private void OnSendNoParametersServerRpc(ServerRpcParams parameters = default) /// Sends multiple parameters to the server /// /// - [ServerRpc(RequireOwnership = false)] - private void OnSendMultiParametersServerRpc(int count, float floatValue, long longValue, ServerRpcParams parameters = default) + [Rpc(SendTo.Server)] + private void OnSendMultiParametersServerRpc(int count, float floatValue, long longValue, RpcParams parameters = default) { m_ClientRpcParamsMultiParameter.Send.TargetClientIds = new[] { parameters.Receive.SenderClientId }; OnSendMultiParametersClientRpc(count, floatValue, longValue, m_ClientRpcParamsMultiParameter); diff --git a/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/ChildObjectScript.cs b/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/ChildObjectScript.cs index decc21ab13..f01e983799 100644 --- a/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/ChildObjectScript.cs +++ b/testproject/Assets/Tests/Manual/InSceneObjectParentingTests/ChildObjectScript.cs @@ -95,8 +95,8 @@ private void PickUpDropItem(NetworkObject player, bool worldPositionStays = true } - [ServerRpc(RequireOwnership = false)] - public void PickupItemServerRpc(bool worldPositionStays = true, ServerRpcParams serverRpcParams = default) + [Rpc(SendTo.Server)] + public void PickupItemServerRpc(bool worldPositionStays = true, RpcParams serverRpcParams = default) { if (NetworkManager.ConnectedClients.ContainsKey(serverRpcParams.Receive.SenderClientId)) { @@ -104,8 +104,8 @@ public void PickupItemServerRpc(bool worldPositionStays = true, ServerRpcParams } } - [ServerRpc(RequireOwnership = false)] - public void DropItemServerRpc(ServerRpcParams serverRpcParams = default) + [Rpc(SendTo.Server)] + public void DropItemServerRpc(RpcParams serverRpcParams = default) { if (NetworkManager.ConnectedClients.ContainsKey(serverRpcParams.Receive.SenderClientId)) { diff --git a/testproject/Assets/Tests/Manual/NestedNetworkTransforms/LerpVsSlerpControls.cs b/testproject/Assets/Tests/Manual/NestedNetworkTransforms/LerpVsSlerpControls.cs index 9cd320255d..955e7939e5 100644 --- a/testproject/Assets/Tests/Manual/NestedNetworkTransforms/LerpVsSlerpControls.cs +++ b/testproject/Assets/Tests/Manual/NestedNetworkTransforms/LerpVsSlerpControls.cs @@ -60,7 +60,7 @@ private void UpdateRotationSpeed(float speed) ChildMover.RotationSpeed = speed; } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void OnSliderUpdatedServerRpc(float sliderValue) { UpdateRotationSpeed(sliderValue); @@ -80,7 +80,7 @@ private void UpdateSlerPosition(bool isOn) ChildMover.SlerpPosition = isOn; } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void OnInterpolateToggleUpdatedServerRpc(bool toggleState) { UpdateSlerPosition(toggleState); diff --git a/testproject/Assets/Tests/Manual/NetworkAnimatorTests/AnimatedCubeController.cs b/testproject/Assets/Tests/Manual/NetworkAnimatorTests/AnimatedCubeController.cs index 7898adb47b..5efbb54fa4 100644 --- a/testproject/Assets/Tests/Manual/NetworkAnimatorTests/AnimatedCubeController.cs +++ b/testproject/Assets/Tests/Manual/NetworkAnimatorTests/AnimatedCubeController.cs @@ -55,7 +55,7 @@ private bool IsOwnerAuthority() } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void ToggleRotateAnimationServerRpc(bool rotate) { m_Rotate = rotate; @@ -82,7 +82,7 @@ internal void ToggleRotateAnimation() } } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void PlayPulseAnimationServerRpc(bool rotate) { m_NetworkAnimator.SetTrigger("Pulse"); @@ -264,4 +264,3 @@ private void LateUpdate() } } } - diff --git a/testproject/Assets/Tests/Manual/Scripts/IntegrationNetworkTransform.cs b/testproject/Assets/Tests/Manual/Scripts/IntegrationNetworkTransform.cs index feeec569f4..9d1243f302 100644 --- a/testproject/Assets/Tests/Manual/Scripts/IntegrationNetworkTransform.cs +++ b/testproject/Assets/Tests/Manual/Scripts/IntegrationNetworkTransform.cs @@ -241,8 +241,8 @@ private void SendStateLogToServer(ulong ownerId) } } - [ServerRpc(RequireOwnership = false)] - private void AddLogEntryServerRpc(HalfPosDebugStates logEntry, ulong ownerId, ServerRpcParams serverRpcParams = default) + [Rpc(SendTo.Server)] + private void AddLogEntryServerRpc(HalfPosDebugStates logEntry, ulong ownerId, RpcParams serverRpcParams = default) { if (!m_FirstInitialStateUpdates.ContainsKey(ownerId)) { @@ -316,7 +316,7 @@ private void DebugTransformStateUpdate(NetworkTransformState oldState, NetworkTr OnNetworkTransformStateUpdate(ref m_NetworkTransformStateUpdate); } -#endif +#endif #if DEBUG_NETWORKTRANSFORM || UNITY_INCLUDE_TESTS /// diff --git a/testproject/Assets/Tests/Manual/Scripts/StatsDisplay.cs b/testproject/Assets/Tests/Manual/Scripts/StatsDisplay.cs index 4d45807013..d1482c0d7c 100644 --- a/testproject/Assets/Tests/Manual/Scripts/StatsDisplay.cs +++ b/testproject/Assets/Tests/Manual/Scripts/StatsDisplay.cs @@ -149,7 +149,7 @@ private void ReceiveStatsClientRpc(StatsInfoContainer statsinfo) /// RPC used to notify server that a specific client wants to receive its stats info /// /// - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void GetStatsServerRPC(ulong clientId) { if (!m_ClientsToUpdate.Contains(clientId)) diff --git a/testproject/Assets/Tests/Runtime/RpcObserverTests.cs b/testproject/Assets/Tests/Runtime/RpcObserverTests.cs index 0db86ccb1a..b71c7e03dd 100644 --- a/testproject/Assets/Tests/Runtime/RpcObserverTests.cs +++ b/testproject/Assets/Tests/Runtime/RpcObserverTests.cs @@ -326,8 +326,8 @@ public void ObserverMessageClientRpc(ClientRpcParams clientRpcParams = default) /// Called by each observer client that received the ObserverMessageClientRpc message /// The sender id is added to the ObserversThatReceivedRPC list /// - [ServerRpc(RequireOwnership = false)] - public void ObserverMessageServerRpc(ServerRpcParams serverRpcParams = default) + [Rpc(SendTo.Server)] + public void ObserverMessageServerRpc(RpcParams serverRpcParams = default) { ObserversThatReceivedRPC.Add(serverRpcParams.Receive.SenderClientId); } diff --git a/testproject/Assets/Tests/Runtime/RpcUserSerializableTypesTest.cs b/testproject/Assets/Tests/Runtime/RpcUserSerializableTypesTest.cs index e9cc45211f..ae10ea60c9 100644 --- a/testproject/Assets/Tests/Runtime/RpcUserSerializableTypesTest.cs +++ b/testproject/Assets/Tests/Runtime/RpcUserSerializableTypesTest.cs @@ -115,7 +115,7 @@ public IEnumerator NetworkSerializableTest() yield return StartServerAndClients(); // [Client-Side] We only need to get the client side Player's NetworkObject so we can grab that instance of the TestSerializationComponent - // Use the client's instance of the + // Use the client's instance of the var targetContext = m_ClientNetworkManagers[0]; // When in distributed authority mode: @@ -994,7 +994,7 @@ private void SendNotOwnerSerializedDataClassRpc(UserSerializableClass userSerial /// Server receives the UserSerializableClass, modifies it, and sends it back /// /// - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void SendServerSerializedDataClassServerRpc(UserSerializableClass userSerializableClass) { ProcessSerializedDataClass(userSerializableClass); @@ -1043,7 +1043,7 @@ private void SendTemplateStructNotOwnerRpc(TemplatedType t1val, TemplatedTy OnTemplateStructUpdated?.Invoke(t1val, t2val, enumVal); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void SendTemplateStructServerRpc(TemplatedType t1val, TemplatedType.NestedTemplatedType t2val, TemplatedType.Enum enumVal) { Debug.Log($"Received server RPC values {t1val.Value} {t2val.Value1} {t2val.Value2} {enumVal}"); @@ -1093,7 +1093,7 @@ private void SendNetworkSerializableTemplateStructNotOwnerRpc(NetworkSerializabl OnNetworkSerializableTemplateStructUpdated?.Invoke(t1val, t2val); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void SendNetworkSerializableTemplateStructServerRpc(NetworkSerializableTemplatedType t1val, NetworkSerializableTemplatedType.NestedTemplatedType t2val) { Debug.Log($"Received NetworkSerializable server RPC values {t1val.Value} {t2val.Value1} {t2val.Value2}"); @@ -1143,7 +1143,7 @@ private void SendTemplateStructNotOwnerRpc(TemplatedType[] t1val, Templated OnTemplateStructsUpdated?.Invoke(t1val, t2val, enumVal); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void SendTemplateStructServerRpc(TemplatedType[] t1val, TemplatedType.NestedTemplatedType[] t2val, TemplatedType.Enum[] enumVal) { Debug.Log($"Received server RPC values {t1val[0].Value} {t2val[0].Value1} {t2val[0].Value2} {enumVal[0]}"); @@ -1194,7 +1194,7 @@ private void SendNetworkSerializableTemplateStructNotOwnerRpc(NetworkSerializabl OnNetworkSerializableTemplateStructsUpdated?.Invoke(t1val, t2val); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void SendNetworkSerializableTemplateStructServerRpc(NetworkSerializableTemplatedType[] t1val, NetworkSerializableTemplatedType.NestedTemplatedType[] t2val) { Debug.Log($"Received NetworkSerializable server RPC values {t1val[0].Value} {t2val[0].Value1} {t2val[0].Value2}"); @@ -1248,7 +1248,7 @@ private void SendClientSerializedDataStructNotOwnerRpc(UserSerializableStruct us /// Server receives the UserSerializableStruct, modifies it, and sends it back /// /// - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void SendServerSerializedDataStructServerRpc(UserSerializableStruct userSerializableStruct) { userSerializableStruct.MyintValue++; @@ -1289,7 +1289,7 @@ public void SendMyObjectClientRpc(MyObject obj) OnMyObjectUpdated?.Invoke(obj); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void SendMyObjectServerRpc(MyObject obj) { OnMyObjectUpdated?.Invoke(obj); @@ -1317,7 +1317,7 @@ public void SendIntListClientRpc(List lst) OnIntListUpdated?.Invoke(lst); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void SendIntListServerRpc(List lst) { OnIntListUpdated?.Invoke(lst); @@ -1345,7 +1345,7 @@ public void SendStringListClientRpc(List lst) OnStringListUpdated?.Invoke(lst); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void SendStringListServerRpc(List lst) { OnStringListUpdated?.Invoke(lst); @@ -1373,7 +1373,7 @@ public void SendMyObjectPassedWithThisRefClientRpc(MyObjectPassedWithThisRef obj OnMyObjectPassedWithThisRefUpdated?.Invoke(obj); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void SendMyObjectPassedWithThisRefServerRpc(MyObjectPassedWithThisRef obj) { OnMyObjectPassedWithThisRefUpdated?.Invoke(obj); @@ -1401,7 +1401,7 @@ public void SendMySharedObjectReferencedByIdClientRpc(MySharedObjectReferencedBy OnMySharedObjectReferencedByIdUpdated?.Invoke(obj); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void SendMySharedObjectReferencedByIdServerRpc(MySharedObjectReferencedById obj) { OnMySharedObjectReferencedByIdUpdated?.Invoke(obj); @@ -1484,7 +1484,7 @@ private void SendClientSerializedDataClassArrayNotOwnerRpc(UserSerializableClass /// that checks the order, and then passes it back to the client /// /// - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void SendServerSerializedDataClassArryServerRpc(UserSerializableClass[] userSerializableClasses) { OnSerializableClassesUpdatedServerRpc?.Invoke(userSerializableClasses); @@ -1549,7 +1549,7 @@ private void SendClientSerializedDataStructArrayNotOwnerRpc(UserSerializableStru /// that checks the order, and then passes it back to the client /// /// - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void SendServerSerializedDataStructArrayServerRpc(UserSerializableStruct[] userSerializableStructs) { OnSerializableStructsUpdatedServerRpc?.Invoke(userSerializableStructs); @@ -1588,7 +1588,7 @@ public void SendMyObjectClientRpc(MyObject[] objs) OnMyObjectUpdated?.Invoke(objs); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void SendMyObjectServerRpc(MyObject[] objs) { OnMyObjectUpdated?.Invoke(objs); @@ -1616,7 +1616,7 @@ public void SendIntListClientRpc(List[] lists) OnIntListUpdated?.Invoke(lists); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void SendIntListServerRpc(List[] lists) { OnIntListUpdated?.Invoke(lists); @@ -1644,7 +1644,7 @@ public void SendStringListClientRpc(List[] lists) OnStringListUpdated?.Invoke(lists); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void SendStringListServerRpc(List[] lists) { OnStringListUpdated?.Invoke(lists); @@ -1672,7 +1672,7 @@ public void SendMyObjectPassedWithThisRefClientRpc(MyObjectPassedWithThisRef[] o OnMyObjectPassedWithThisRefUpdated?.Invoke(objs); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void SendMyObjectPassedWithThisRefServerRpc(MyObjectPassedWithThisRef[] objs) { OnMyObjectPassedWithThisRefUpdated?.Invoke(objs); @@ -1701,7 +1701,7 @@ public void SendMySharedObjectReferencedByIdClientRpc(MySharedObjectReferencedBy OnMySharedObjectReferencedByIdUpdated?.Invoke(objs); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void SendMySharedObjectReferencedByIdServerRpc(MySharedObjectReferencedById[] objs) { OnMySharedObjectReferencedByIdUpdated?.Invoke(objs); diff --git a/testproject/Assets/Tests/Runtime/ServerDisconnectsClientTest.cs b/testproject/Assets/Tests/Runtime/ServerDisconnectsClientTest.cs index d93f038a78..c16cceda66 100644 --- a/testproject/Assets/Tests/Runtime/ServerDisconnectsClientTest.cs +++ b/testproject/Assets/Tests/Runtime/ServerDisconnectsClientTest.cs @@ -47,7 +47,7 @@ public override void OnNetworkSpawn() base.OnNetworkSpawn(); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void ClientToServerRpc() { Debug.Log($"Received {nameof(ClientToServerRpc)}"); diff --git a/testproject/Assets/Tests/Runtime/Support/ShutdownDuringOnNetworkSpawnBehaviour.cs b/testproject/Assets/Tests/Runtime/Support/ShutdownDuringOnNetworkSpawnBehaviour.cs index 33b10b588d..7a8e303fa4 100644 --- a/testproject/Assets/Tests/Runtime/Support/ShutdownDuringOnNetworkSpawnBehaviour.cs +++ b/testproject/Assets/Tests/Runtime/Support/ShutdownDuringOnNetworkSpawnBehaviour.cs @@ -29,7 +29,7 @@ private void TestClientRpc() ++ClientRpcsCalled; } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] private void TestServerRpc() { ++ServerRpcsCalled; diff --git a/testproject/Legacy/MultiprocessRuntime/TestCoordinator.cs b/testproject/Legacy/MultiprocessRuntime/TestCoordinator.cs index e9a7bdeb4f..6eeafd23b0 100644 --- a/testproject/Legacy/MultiprocessRuntime/TestCoordinator.cs +++ b/testproject/Legacy/MultiprocessRuntime/TestCoordinator.cs @@ -402,7 +402,7 @@ public static Func ConsumeClientIsFinished(ulong clientId, bool useTimeout }; } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void ClientFinishedServerRpc(ServerRpcParams p = default) { // signal from clients to the server to say the client is done with it's task @@ -487,7 +487,7 @@ public void KeepAliveClientRpc() m_TimeSinceLastKeepAlive = Time.time; } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void WriteTestResultsServerRpc(float result, ServerRpcParams receiveParams = default) { var senderId = receiveParams.Receive.SenderClientId; @@ -506,16 +506,15 @@ public void WriteTestResultsServerRpc(float result, ServerRpcParams receiveParam /// /// Use to log server-side without MultiprocessLogger formatting. /// - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void WriteErrorServerRpc(string errorMessage, ServerRpcParams receiveParams = default) { MultiprocessLogger.LogError($"[Netcode-Server Sender={receiveParams.Receive.SenderClientId}] {errorMessage}"); } - [ServerRpc(RequireOwnership = false)] + [Rpc(SendTo.Server)] public void WriteLogServerRpc(string logMessage, ServerRpcParams receiveParams = default) { MultiprocessLogger.Log($"[Netcode-Server Sender={receiveParams.Receive.SenderClientId}] {logMessage}"); } } - From df16b48e75c04b03e617ba7cc8c9791cbd0b645f Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 13:47:50 -0400 Subject: [PATCH 12/27] small fixes --- .../advanced-topics/message-system/rpc.md | 2 +- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 2 +- .../Runtime/Messaging/RpcAttributes.cs | 12 ++++++------ .../Tests/Runtime/Rpc/RpcInvocationTests.cs | 5 ++--- .../Tests/Runtime/Rpc/RpcManyClientsTests.cs | 2 +- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md index 838ad3f5d6..b50d9af233 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md @@ -1,6 +1,6 @@ # RPC -Any process can communicate with any other process by sending a remote procedure call (RPC). As of Netcode for GameObjects version 1.8.0, the `Rpc` attribute encompasses server to client RPCs, client to server RPCs, and client to client RPCs. +Any process can communicate with any other process by sending a remote procedure call (RPC). The `Rpc` attribute is used to define who receives and executes the RPC. ![](../../images/sequence_diagrams/RPCs/ServerRPCs.png) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index f08e5783cd..b95074b66f 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -612,7 +612,7 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly, private const string k_RpcAttribute_InvokePermission = nameof(RpcAttribute.InvokePermission); #pragma warning disable CS0618 // Type or member is obsolete -// Need to ignore the obsolete warning as the obsolete behaviour still needs to work + // Need to ignore the obsolete warning as the obsolete behaviour still needs to work private const string k_ServerRpcAttribute_RequireOwnership = nameof(ServerRpcAttribute.RequireOwnership); #pragma warning restore CS0618 // Type or member is obsolete diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs index 1fbfa5979d..7fd2fd9fd9 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs @@ -40,7 +40,8 @@ public enum RpcInvokePermission } /// - /// Represents the common base class for Rpc attributes. + /// Marks a method as a remote procedure call (RPC). + /// The marked method will be executed on all game instances defined by the target. /// [AttributeUsage(AttributeTargets.Method)] public class RpcAttribute : Attribute @@ -80,7 +81,7 @@ public struct RpcAttributeParams public RpcDelivery Delivery = RpcDelivery.Reliable; /// - /// Who has network permission to invoke this RPC + /// Controls who has permission to invoke this RPC. The default setting is /// public RpcInvokePermission InvokePermission; @@ -90,7 +91,7 @@ public struct RpcAttributeParams /// /// Deprecated in favor of . /// - [Obsolete("RequireOwnership is deprecated. Please use InvokePermission = RpcInvokePermission.Owner instead.")] + [Obsolete("RequireOwnership is deprecated. Please use InvokePermission = RpcInvokePermission.Owner or InvokePermission = RpcInvokePermission.Everyone instead.")] public bool RequireOwnership; /// @@ -127,15 +128,14 @@ public class ServerRpcAttribute : RpcAttribute { /// /// When true, only the owner of the NetworkObject can invoke this ServerRpc. - /// This property sets the base to when "RequireOwnership = true" or to when "RequireOwnership = false". />. /// /// - /// Deprecated in favor of using with and an . + /// Deprecated in favor of using with a target and an . /// /// [ServerRpc(RequireOwnership = false)] /// // is replaced with /// [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] - /// // or, as InvokePermission has a default setting of RpcInvokePermission.Everyone, you can also use + /// // as InvokePermission has a default setting of RpcInvokePermission.Everyone, you can also use /// [Rpc(SendTo.Server)] /// /// diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs index 739f8fd126..c0c1f50a94 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -13,11 +12,11 @@ namespace Unity.Netcode.RuntimeTests.Rpc { [TestFixture(NetworkTopologyTypes.DistributedAuthority)] [TestFixture(NetworkTopologyTypes.ClientServer)] - internal class RpcInvocationTests: NetcodeIntegrationTest + internal class RpcInvocationTests : NetcodeIntegrationTest { protected override int NumberOfClients => 3; - public RpcInvocationTests(NetworkTopologyTypes topologyType) : base(topologyType) {} + public RpcInvocationTests(NetworkTopologyTypes topologyType) : base(topologyType) { } private GameObject m_Prefab; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs index 502715dba9..62fb94bbe5 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcManyClientsTests.cs @@ -127,7 +127,7 @@ public void RpcManyClientsTest() rpcManyClientsObject.ReceivedFrom.Clear(); rpcManyClientsObject.Count = 0; - var target = new [] { m_ClientNetworkManagers[1].LocalClientId, m_ClientNetworkManagers[2].LocalClientId }; + var target = new[] { m_ClientNetworkManagers[1].LocalClientId, m_ClientNetworkManagers[2].LocalClientId }; param.Send.Target = rpcManyClientsObject.RpcTarget.Group(target, RpcTargetUse.Temp); rpcManyClientsObject.WithParamsClientRpc(param); From 943b488c276140a16e4bafa03ac12dace6479150 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 13:54:06 -0400 Subject: [PATCH 13/27] Add back RpcAttributeParams.RequireOwnership --- .../Runtime/Messaging/RpcAttributes.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs index 7fd2fd9fd9..59555210c3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcAttributes.cs @@ -56,6 +56,15 @@ public struct RpcAttributeParams /// public RpcDelivery Delivery; + /// + /// When true, only the owner of the object can execute this RPC + /// + /// + /// Deprecated in favor of . + /// + [Obsolete("RequireOwnership is deprecated. Please use InvokePermission instead.")] + public bool RequireOwnership; + /// /// Who has network permission to invoke this RPC /// @@ -70,8 +79,6 @@ public struct RpcAttributeParams /// When true, allows the RPC target to be overridden at runtime /// public bool AllowTargetOverride; - - public bool RequireOwnership; } // Must match the fields in RemoteAttributeParams From a45d5f7023ce3d53346ef42f01574c70a1058be6 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 14:31:04 -0400 Subject: [PATCH 14/27] Update CHANGELOG --- com.unity.netcode.gameobjects/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index ab7434e0fa..a3969ee37a 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,6 +10,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added +- `RpcInvokePermission` to control who has permission to invoke specific RPC methods. (#3731) - Added NetworkRigidbody documentation section. (#3664) ### Changed @@ -24,10 +25,10 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Deprecated +- Deprecated all `RequireOwnership` fields around the RPCs in favor of the `RpcInvokePermission`. (#3731) ### Removed - ### Fixed - Multiple disconnect events from the same transport will no longer disconnect the host. (#3707) From e1aac0b783d74b845334de5115d7105f1dd1151f Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 14:31:41 -0400 Subject: [PATCH 15/27] Add InvokePermission attribute when RequireOwnership is defined --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 33 ++++++++--- .../Runtime/Core/NetworkBehaviour.cs | 2 +- .../Tests/Runtime/Rpc/RpcInvocationTests.cs | 59 +++++++++---------- 3 files changed, 55 insertions(+), 39 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index b95074b66f..0835aca46a 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -470,6 +470,7 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly, private FieldReference m_UniversalRpcParams_Receive_SenderClientId_FieldRef; private TypeReference m_UniversalRpcParams_TypeRef; private TypeReference m_ClientRpcParams_TypeRef; + private TypeReference m_RpcInvokePermissions_TypeRef; private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedByMemcpy_MethodRef; private MethodReference m_NetworkVariableSerializationTypes_InitializeSerializer_UnmanagedByMemcpyArray_MethodRef; #if UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT @@ -656,6 +657,7 @@ private bool ImportReferences(ModuleDefinition moduleDefinition, string[] assemb TypeDefinition serverRpcParamsTypeDef = null; TypeDefinition clientRpcParamsTypeDef = null; TypeDefinition universalRpcParamsTypeDef = null; + TypeDefinition rpcInvokePermissionTypeDef = null; TypeDefinition fastBufferWriterTypeDef = null; TypeDefinition fastBufferReaderTypeDef = null; TypeDefinition networkVariableSerializationTypesTypeDef = null; @@ -717,6 +719,12 @@ private bool ImportReferences(ModuleDefinition moduleDefinition, string[] assemb continue; } + if (rpcInvokePermissionTypeDef == null && netcodeTypeDef.Name == nameof(RpcInvokePermission)) + { + rpcInvokePermissionTypeDef = netcodeTypeDef; + continue; + } + if (fastBufferWriterTypeDef == null && netcodeTypeDef.Name == nameof(FastBufferWriter)) { fastBufferWriterTypeDef = netcodeTypeDef; @@ -942,6 +950,7 @@ private bool ImportReferences(ModuleDefinition moduleDefinition, string[] assemb } m_ClientRpcParams_TypeRef = moduleDefinition.ImportReference(clientRpcParamsTypeDef); + m_RpcInvokePermissions_TypeRef = moduleDefinition.ImportReference(rpcInvokePermissionTypeDef); m_FastBufferWriter_TypeRef = moduleDefinition.ImportReference(fastBufferWriterTypeDef); m_FastBufferReader_TypeRef = moduleDefinition.ImportReference(fastBufferReaderTypeDef); @@ -1638,14 +1647,18 @@ private CustomAttribute CheckAndGetRpcAttribute(MethodDefinition methodDefinitio return null; } - bool hasRequireOwnership = false, hasInvokePermission = false; + var typeSystem = methodDefinition.Module.TypeSystem; + var hasInvokePermission = false; - foreach (var argument in rpcAttribute.Fields) + CustomAttributeNamedArgument? invokePermissionAttribute = null; + foreach(var argument in rpcAttribute.Fields) { switch (argument.Name) { case k_ServerRpcAttribute_RequireOwnership: - hasRequireOwnership = true; + var requireOwnership = argument.Argument.Type == typeSystem.Boolean && (bool)argument.Argument.Value; + var invokePermissionArg = new CustomAttributeArgument(m_RpcInvokePermissions_TypeRef, requireOwnership ? RpcInvokePermission.Owner : RpcInvokePermission.Everyone); + invokePermissionAttribute = new CustomAttributeNamedArgument( k_RpcAttribute_InvokePermission, invokePermissionArg); break; case k_RpcAttribute_InvokePermission: hasInvokePermission = true; @@ -1653,12 +1666,18 @@ private CustomAttribute CheckAndGetRpcAttribute(MethodDefinition methodDefinitio } } - if (hasRequireOwnership && hasInvokePermission) + if (invokePermissionAttribute != null) { - m_Diagnostics.AddError("Rpc attribute cannot declare both RequireOwnership and InvokePermission!"); - return null; + if (hasInvokePermission) + { + m_Diagnostics.AddError($"{methodDefinition.Name} cannot declare both RequireOwnership and InvokePermission!"); + return null; + } + + rpcAttribute.Fields.Add(invokePermissionAttribute.Value); } + // Checks for IsSerializable are moved to later as the check is now done by dynamically seeing if any valid // serializer OR extension method exists for it. return rpcAttribute; @@ -2922,8 +2941,6 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition var processor = rpcHandler.Body.GetILProcessor(); var isServerRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ServerRpcAttribute_FullName; - var isClientRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.ClientRpcAttribute_FullName; - var isGenericRpc = rpcAttribute.AttributeType.FullName == CodeGenHelpers.RpcAttribute_FullName; var requireOwnership = true; // default value MUST be == `ServerRpcAttribute.RequireOwnership` foreach (var attrField in rpcAttribute.Fields) { diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 384c45f422..5733e2e9cb 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -335,7 +335,7 @@ internal FastBufferWriter __beginSendRpc(uint rpcMethodId, RpcParams rpcParams, throw new RpcException("This RPC can only be sent by the server."); } - if ((attributeParams.RequireOwnership || attributeParams.InvokePermission == RpcInvokePermission.Owner) && !IsOwner) + if (attributeParams.InvokePermission == RpcInvokePermission.Owner && !IsOwner) { throw new RpcException("This RPC can only be sent by its owner."); } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs index c0c1f50a94..9f794e8e56 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs @@ -22,6 +22,9 @@ public RpcInvocationTests(NetworkTopologyTypes topologyType) : base(topologyType private Dictionary m_InvokeInstances = new(); + // TODO: [CmbServiceTests] Enable once the CMB service fixes the client spoofing issue. + protected override bool UseCMBService() => false; + protected override void OnServerAndClientsCreated() { m_Prefab = CreateNetworkObjectPrefab("RpcInvokePermissionTest"); @@ -188,42 +191,38 @@ public IEnumerator RpcInvokePermissionReceivingTests() yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); AssertOnTimeout("[InvokePermissions.Everyone] Incorrect Rpc calls received"); - // DANGO-TODO: Fix the client spoofing issue - if (!m_UseCmbService) - { - var firstClientInstance = m_InvokeInstances[firstClient]; - var secondClient = GetNonAuthorityNetworkManager(1); - var thirdClient = GetNonAuthorityNetworkManager(2); + var firstClientInstance = m_InvokeInstances[firstClient]; + var secondClient = GetNonAuthorityNetworkManager(1); + var thirdClient = GetNonAuthorityNetworkManager(2); - firstClientInstance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.TrackSenderIdRpc)] = 1; + firstClientInstance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.TrackSenderIdRpc)] = 1; - // Manually set the senderId to an incorrect value - var secondClientInstance = m_InvokeInstances[secondClient]; - var bufferWriter = new FastBufferWriter(1024, Allocator.Temp); - using (bufferWriter) + // Manually set the senderId to an incorrect value + var secondClientInstance = m_InvokeInstances[secondClient]; + var bufferWriter = new FastBufferWriter(1024, Allocator.Temp); + using (bufferWriter) + { + var rpcMessage = new RpcMessage { - var rpcMessage = new RpcMessage + Metadata = new RpcMetadata { - Metadata = new RpcMetadata - { - NetworkObjectId = secondClientInstance.NetworkObjectId, - NetworkBehaviourId = secondClientInstance.NetworkBehaviourId, - NetworkRpcMethodId = GetMethodIdFromMethodName(nameof(InvokePermissionBehaviour.TrackSenderIdRpc)), - }, - // Set the sender to the third client - SenderClientId = thirdClient.LocalClientId, - WriteBuffer = bufferWriter - }; - - // Send the message on the second client - secondClientInstance.RpcTarget.Owner.Send(secondClientInstance, ref rpcMessage, NetworkDelivery.Reliable, new RpcParams()); - } + NetworkObjectId = secondClientInstance.NetworkObjectId, + NetworkBehaviourId = secondClientInstance.NetworkBehaviourId, + NetworkRpcMethodId = GetMethodIdFromMethodName(nameof(InvokePermissionBehaviour.TrackSenderIdRpc)), + }, + // Set the sender to the third client + SenderClientId = thirdClient.LocalClientId, + WriteBuffer = bufferWriter + }; + + // Send the message on the second client + secondClientInstance.RpcTarget.Owner.Send(secondClientInstance, ref rpcMessage, NetworkDelivery.Reliable, new RpcParams()); + } - yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); - AssertOnTimeout("[SpoofedSenderId] Incorrect Rpc calls received"); + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout("[SpoofedSenderId] Incorrect Rpc calls received"); - Assert.That(firstClientInstance.SenderIdReceived, Is.EqualTo(secondClient.LocalClientId), "Received spoofed sender id!"); - } + Assert.That(firstClientInstance.SenderIdReceived, Is.EqualTo(secondClient.LocalClientId), "Received spoofed sender id!"); } private void SendUncheckedMessage(NetworkManager manager, InvokePermissionBehaviour invokePermissionsObject, string rpcMethodName) From 7985cc7509f5d2333511453b588d33a9b5f23e8d Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 14:33:33 -0400 Subject: [PATCH 16/27] Fix whitespace --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index 0835aca46a..76138dc507 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -1651,14 +1651,14 @@ private CustomAttribute CheckAndGetRpcAttribute(MethodDefinition methodDefinitio var hasInvokePermission = false; CustomAttributeNamedArgument? invokePermissionAttribute = null; - foreach(var argument in rpcAttribute.Fields) + foreach (var argument in rpcAttribute.Fields) { switch (argument.Name) { case k_ServerRpcAttribute_RequireOwnership: var requireOwnership = argument.Argument.Type == typeSystem.Boolean && (bool)argument.Argument.Value; var invokePermissionArg = new CustomAttributeArgument(m_RpcInvokePermissions_TypeRef, requireOwnership ? RpcInvokePermission.Owner : RpcInvokePermission.Everyone); - invokePermissionAttribute = new CustomAttributeNamedArgument( k_RpcAttribute_InvokePermission, invokePermissionArg); + invokePermissionAttribute = new CustomAttributeNamedArgument(k_RpcAttribute_InvokePermission, invokePermissionArg); break; case k_RpcAttribute_InvokePermission: hasInvokePermission = true; From 0408a07716e54760e3160ac7af3cb6d652af1d55 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 15:22:17 -0400 Subject: [PATCH 17/27] fix __registerRpc breaking change --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 2 +- com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs | 2 +- .../Tests/Runtime/Rpc/RpcInvocationTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index 76138dc507..f87345cd55 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -1478,8 +1478,8 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass instructions.Add(processor.Create(OpCodes.Ldnull)); instructions.Add(processor.Create(OpCodes.Ldftn, callMethod)); instructions.Add(processor.Create(OpCodes.Newobj, m_NetworkHandlerDelegateCtor_MethodRef)); - instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)invokePermission)); instructions.Add(processor.Create(OpCodes.Ldstr, rpcMethodName)); + instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)invokePermission)); instructions.Add(processor.Create(OpCodes.Call, m_NetworkBehaviour___registerRpc_MethodRef)); } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 5733e2e9cb..20b456cacd 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -942,7 +942,7 @@ internal virtual void __initializeRpcs() #pragma warning disable IDE1006 // disable naming rule violation check // RuntimeAccessModifiersILPP will make this `protected` - internal void __registerRpc(uint hash, RpcReceiveHandler handler, RpcInvokePermission permission, string rpcMethodName) + internal void __registerRpc(uint hash, RpcReceiveHandler handler, string rpcMethodName, RpcInvokePermission permission = RpcInvokePermission.Everyone) #pragma warning restore IDE1006 // restore naming rule violation check { __rpc_func_table[GetType()][hash] = handler; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs index 9f794e8e56..100faba4d0 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs @@ -8,7 +8,7 @@ using UnityEngine; using UnityEngine.TestTools; -namespace Unity.Netcode.RuntimeTests.Rpc +namespace Unity.Netcode.RuntimeTests { [TestFixture(NetworkTopologyTypes.DistributedAuthority)] [TestFixture(NetworkTopologyTypes.ClientServer)] From df8458c1837157c83f443fe338d0bdc9825a301c Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 16:03:28 -0400 Subject: [PATCH 18/27] second try at fixing breaking change --- .../Runtime/Core/NetworkBehaviour.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 20b456cacd..92b4679fdf 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -941,8 +941,10 @@ internal virtual void __initializeRpcs() } #pragma warning disable IDE1006 // disable naming rule violation check + // This is needed to add the RpcInvokePermission as even with an optional parameter, the change counts as a breaking change. + internal void __registerRpc(uint hash, RpcReceiveHandler handler, string rpcMethodName) => __registerRpc(hash, handler, rpcMethodName, RpcInvokePermission.Everyone); // RuntimeAccessModifiersILPP will make this `protected` - internal void __registerRpc(uint hash, RpcReceiveHandler handler, string rpcMethodName, RpcInvokePermission permission = RpcInvokePermission.Everyone) + internal void __registerRpc(uint hash, RpcReceiveHandler handler, string rpcMethodName, RpcInvokePermission permission) #pragma warning restore IDE1006 // restore naming rule violation check { __rpc_func_table[GetType()][hash] = handler; From 25ac8029700bfbbc912092c071db9d662fea85d8 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 19:19:53 -0400 Subject: [PATCH 19/27] Fix ilpp to log message if NetworkManager is not connected or listening --- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 19 ++------- .../Editor/NetworkBehaviourEditorTests.cs | 40 +++++++++++++++++-- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index f87345cd55..54271e1b65 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -2229,6 +2229,7 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA { var returnInstr = processor.Create(OpCodes.Ret); var lastInstr = processor.Create(OpCodes.Nop); + var logNextInstr = processor.Create(OpCodes.Nop); // networkManager = this.NetworkManager; instructions.Add(processor.Create(OpCodes.Ldarg_0)); @@ -2237,28 +2238,16 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA // if (networkManager == null || !networkManager.IsListening) { ... return }; instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); - instructions.Add(processor.Create(OpCodes.Brfalse, returnInstr)); + instructions.Add(processor.Create(OpCodes.Brfalse_S, logNextInstr)); instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); instructions.Add(processor.Create(OpCodes.Callvirt, m_NetworkManager_getIsListening_MethodRef)); - instructions.Add(processor.Create(OpCodes.Brtrue, lastInstr)); - - var logNextInstr = processor.Create(OpCodes.Nop); - - // if (LogLevel.Normal > networkManager.LogLevel) - instructions.Add(processor.Create(OpCodes.Ldloc, netManLocIdx)); - instructions.Add(processor.Create(OpCodes.Ldfld, m_NetworkManager_LogLevel_FieldRef)); - instructions.Add(processor.Create(OpCodes.Ldc_I4, (int)LogLevel.Normal)); - instructions.Add(processor.Create(OpCodes.Cgt)); - instructions.Add(processor.Create(OpCodes.Ldc_I4, 0)); - instructions.Add(processor.Create(OpCodes.Ceq)); - instructions.Add(processor.Create(OpCodes.Brfalse, logNextInstr)); + instructions.Add(processor.Create(OpCodes.Brtrue_S, lastInstr)); // Debug.LogError(...); + instructions.Add(logNextInstr); instructions.Add(processor.Create(OpCodes.Ldstr, "Rpc methods can only be invoked after starting the NetworkManager!")); instructions.Add(processor.Create(OpCodes.Call, m_Debug_LogError_MethodRef)); - instructions.Add(logNextInstr); - instructions.Add(returnInstr); instructions.Add(lastInstr); } diff --git a/com.unity.netcode.gameobjects/Tests/Editor/NetworkBehaviourEditorTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/NetworkBehaviourEditorTests.cs index 97e7343131..ca7d121ea5 100644 --- a/com.unity.netcode.gameobjects/Tests/Editor/NetworkBehaviourEditorTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Editor/NetworkBehaviourEditorTests.cs @@ -1,5 +1,7 @@ +using System.Collections; using NUnit.Framework; using UnityEngine; +using UnityEngine.TestTools; using Object = UnityEngine.Object; namespace Unity.Netcode.EditorTests @@ -35,8 +37,6 @@ public void AccessNetworkObjectTest() var gameObject = new GameObject(nameof(AccessNetworkObjectTest)); var networkBehaviour = gameObject.AddComponent(); - networkBehaviour.NoNetworkRpc(); - Assert.That(networkBehaviour.NetworkObject, Is.Null); var networkObject = gameObject.AddComponent(); @@ -71,6 +71,34 @@ public void AccessNetworkObjectTestInDerivedClassWithOverrideFunctions() Object.DestroyImmediate(gameObject); } + [UnityTest] + public IEnumerator RpcShouldNoopWhenInvokedWithoutANetworkManagerSession() + { + var noNetworkError = "Rpc methods can only be invoked after starting the NetworkManager!"; + var gameObject = new GameObject(nameof(AccessNetworkObjectTestInDerivedClassWithOverrideFunctions)); + var networkBehaviour = gameObject.AddComponent(); + + // No networkManager exists so error should be logged + LogAssert.Expect(LogType.Error, noNetworkError); + networkBehaviour.NoNetworkRpc(); + + // Ensure RPC was not invoked locally + yield return null; + Assert.That(networkBehaviour.RpcWasInvoked, Is.False); + + var networkManager = gameObject.AddComponent(); + networkManager.SetSingleton(); + + LogAssert.Expect(LogType.Error, noNetworkError); + networkBehaviour.NoNetworkRpc(); + + // Ensure RPC was not invoked locally + yield return null; + Assert.That(networkBehaviour.RpcWasInvoked, Is.False); + + Object.DestroyImmediate(gameObject); + } + // Note: in order to repro https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/issues/1078 // this child class must be defined before its parent to assure it is processed first by ILPP internal class DerivedNetworkBehaviour : EmptyNetworkBehaviour @@ -79,10 +107,16 @@ internal class DerivedNetworkBehaviour : EmptyNetworkBehaviour internal class EmptyNetworkBehaviour : NetworkBehaviour { + } + + internal class RpcNetworkBehaviour : NetworkBehaviour + { + public bool RpcWasInvoked; + [Rpc(SendTo.Everyone)] public void NoNetworkRpc() { - Debug.Log("No Network Rpc"); + RpcWasInvoked = true; } } } From 4abce68195f20c7bd93ff5f7718c82cdac945efb Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 19:21:01 -0400 Subject: [PATCH 20/27] Update com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md Co-authored-by: Noel Stephens --- .../Documentation~/advanced-topics/message-system/rpc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md index b50d9af233..3fe49023ed 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md @@ -1,6 +1,6 @@ # RPC -Any process can communicate with any other process by sending a remote procedure call (RPC). The `Rpc` attribute is used to define who receives and executes the RPC. +Any process can communicate with any other process by sending a remote procedure call (RPC). The `Rpc` attribute is the recommended attribute to use when declaring RPC methods. The `Rpc` attribute's parameters defines the receivers and execution rights for the RPC method. ![](../../images/sequence_diagrams/RPCs/ServerRPCs.png) From 961010360c713f714bd8dcf2ad6305aa8f48ee24 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 19:21:22 -0400 Subject: [PATCH 21/27] Update com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md Co-authored-by: Noel Stephens --- .../Documentation~/advanced-topics/message-system/rpc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md index 3fe49023ed..54a45dc5a0 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md @@ -204,7 +204,7 @@ There are a few other parameters that can be passed to either the `Rpc` attribut | Parameter | Description | | ----------------------- | ------------------------------------------------------------ | | `Delivery` | Controls whether the delivery is reliable (default) or unreliable.

Options: `RpcDelivery.Reliable` or `RpcDelivery.Unreliable`
Default: `RpcDelivery.Reliable` | -| `InvokePermission` | Controls how an RPC is allowed to be invoked.

Options:
`RpcInvokePermission.Server` - This RPC throws an exception if invoked by a game client that is not the server.
`RpcInvokePermission.Owner` - This RPC throws an exception if invoked by a game client that does not own the object.
`RpcInvokePermission.Everyone` - This can be invoked by any connected game client.
Default: `RpcInvokePermission.Everyone` | +| `InvokePermission` | Sets an RPC 's invocation permissions.

Options:
`RpcInvokePermission.Server` - This RPC throws an exception if invoked by a game client that is not the server.
`RpcInvokePermission.Owner` - This RPC throws an exception if invoked by a game client that does not own the object.
`RpcInvokePermission.Everyone` - This can be invoked by any connected game client.
Default: `RpcInvokePermission.Everyone` | | `DeferLocal` | If `true`, RPCs that execute locally will be deferred until the start of the next frame, as if they had been sent over the network. (They will not actually be sent over the network, but will be treated as if they were.) This is useful for mutually recursive RPCs on hosts, where sending back and forth between the server and the "host client" will cause a stack overflow if each RPC is executed instantly; simulating the flow of RPCs between remote client and server enables this flow to work the same in both contexts.

Default: `false` | | `AllowTargetOverride` | By default, any `SendTo` value other than `SendTo.SpecifiedInParams` is a hard-coded value that cannot be changed. Setting this to `true` allows you to provide an alternate target at runtime, while using the `SendTo` value as a fallback if no runtime value is provided. | From e09f83e8face6622dc52d12054714be39b27a6c2 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 20 Oct 2025 19:22:02 -0400 Subject: [PATCH 22/27] Update com.unity.netcode.gameobjects/Documentation~/terms-concepts/ownership.md Co-authored-by: Noel Stephens --- .../Documentation~/terms-concepts/ownership.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/terms-concepts/ownership.md b/com.unity.netcode.gameobjects/Documentation~/terms-concepts/ownership.md index 1d0427c1ce..d87c2eb96a 100644 --- a/com.unity.netcode.gameobjects/Documentation~/terms-concepts/ownership.md +++ b/com.unity.netcode.gameobjects/Documentation~/terms-concepts/ownership.md @@ -8,7 +8,7 @@ Netcode for GameObjects also supports building games with a [distributed authori ## Ownership in client-server -In a client-server topology, the server has ultimate authority over all NetworkObjects. Clients can request ownership of [specific objects](../components/core/networkobject.md#ownership), but the server has the final say in whether to grant or deny these requests.x +In a client-server topology, the server has ultimate authority over all NetworkObjects. Clients can request ownership of [specific objects](../components/core/networkobject.md#ownership), but the server has the final say in whether to grant or deny these requests. ## Ownership in distributed authority From 2b2fb735995e89fbb19248463caa805103a874d5 Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 21 Oct 2025 16:08:31 -0400 Subject: [PATCH 23/27] Update log messages --- .../Runtime/Messaging/Messages/ProxyMessage.cs | 12 ++++++++---- .../Runtime/Messaging/Messages/RpcMessages.cs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs index 5d2fa4843c..95b4d41a49 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ProxyMessage.cs @@ -60,6 +60,10 @@ public unsafe void Handle(ref NetworkContext context) // Do not handle the message if the sender does not have permission to do so. if (!hasPermission) { + if (networkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogErrorServer($"Rpc message received from client-{context.SenderId} who does not have permission to perform this operation!"); + } return; } @@ -68,20 +72,20 @@ public unsafe void Handle(ref NetworkContext context) var nonServerIds = new NativeList(Allocator.Temp); - for (var i = 0; i < TargetClientIds.Length; ++i) + foreach (var client in TargetClientIds) { - if (!observers.Contains(TargetClientIds[i])) + if (!observers.Contains(client)) { continue; } - if (TargetClientIds[i] == NetworkManager.ServerClientId) + if (client == NetworkManager.ServerClientId) { WrappedMessage.Handle(ref context); } else { - nonServerIds.Add(TargetClientIds[i]); + nonServerIds.Add(client); } } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs index d0ce5f717b..d37015fdcd 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs @@ -80,7 +80,7 @@ public static void Handle(ref NetworkContext context, ref RpcMetadata metadata, { if (networkManager.LogLevel <= LogLevel.Developer) { - Debug.LogError("Rpc message received from a client who does not have permission to perform this operation!"); + NetworkLog.LogErrorServer($"Rpc message received from client-{rpcParams.SenderId} who does not have permission to perform this operation!"); } return; } From 760921fa4f21f93e56214a4435b6476792ceac1a Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 23 Oct 2025 12:47:13 -0400 Subject: [PATCH 24/27] Fix naming in networkvariable.md doc --- .../Documentation~/basics/networkvariable.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md index d5bd4cf875..3aa5865386 100644 --- a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md +++ b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md @@ -145,7 +145,7 @@ This works the same way with dynamically spawned NetworkObjects. The [synchronization and notification example](#synchronization-and-notification-example) highlights the differences between synchronizing a `NetworkVariable` with newly-joining clients and notifying connected clients when a `NetworkVariable` changes, but it doesn't provide any concrete example usage. -The `OnValueChanged` example shows a simple server-authoritative `NetworkVariable` being used to track the state of a door (that is, open or closed) using an RPC that is sent to the server. Each time the door is used by a client, the `Door.ToggleServerRpc` is invoked and the server-side toggles the state of the door. When the `Door.State.Value` changes, all connected clients are synchronized to the (new) current `Value` and the `OnStateChanged` method is invoked locally on each client. +The `OnValueChanged` example shows a simple server-authoritative `NetworkVariable` being used to track the state of a door (that is, open or closed) using an RPC that is sent to the server. Each time the door is used by a client, the `Door.ToggleStateRpc` is invoked and the server-side toggles the state of the door. When the `Door.State.Value` changes, all connected clients are synchronized to the (new) current `Value` and the `OnStateChanged` method is invoked locally on each client. ```csharp public class Door : NetworkBehaviour @@ -180,7 +180,7 @@ public class Door : NetworkBehaviour } [Rpc(SendTo.Server)] - public void ToggleServerRpc() + public void ToggleStateRpc() { // this will cause a replication over the network // and ultimately invoke `OnValueChanged` on receivers From 834eedf9f786bbb502e6cc2fe44e76e68253f5cf Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 23 Oct 2025 15:12:53 -0400 Subject: [PATCH 25/27] Update com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md Co-authored-by: Noel Stephens --- .../Documentation~/advanced-topics/message-system/rpc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md index 54a45dc5a0..75629b9e44 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md @@ -1,6 +1,6 @@ # RPC -Any process can communicate with any other process by sending a remote procedure call (RPC). The `Rpc` attribute is the recommended attribute to use when declaring RPC methods. The `Rpc` attribute's parameters defines the receivers and execution rights for the RPC method. +Any process can communicate with any other process by sending a remote procedure call (RPC). The `Rpc` attribute is the recommended attribute to use when declaring RPC methods. The `Rpc` attribute's parameters define the receivers and execution rights for the RPC method. ![](../../images/sequence_diagrams/RPCs/ServerRPCs.png) From b3ea00a3de0e45bc65a2776978146d751354d98d Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 23 Oct 2025 15:12:59 -0400 Subject: [PATCH 26/27] Update com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md Co-authored-by: Noel Stephens --- .../Documentation~/advanced-topics/message-system/rpc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md index 75629b9e44..56ea902ae9 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md @@ -204,7 +204,7 @@ There are a few other parameters that can be passed to either the `Rpc` attribut | Parameter | Description | | ----------------------- | ------------------------------------------------------------ | | `Delivery` | Controls whether the delivery is reliable (default) or unreliable.

Options: `RpcDelivery.Reliable` or `RpcDelivery.Unreliable`
Default: `RpcDelivery.Reliable` | -| `InvokePermission` | Sets an RPC 's invocation permissions.

Options:
`RpcInvokePermission.Server` - This RPC throws an exception if invoked by a game client that is not the server.
`RpcInvokePermission.Owner` - This RPC throws an exception if invoked by a game client that does not own the object.
`RpcInvokePermission.Everyone` - This can be invoked by any connected game client.
Default: `RpcInvokePermission.Everyone` | +| `InvokePermission` | Sets an RPC's invocation permissions.

Options:
`RpcInvokePermission.Server` - This RPC throws an exception if invoked by a game client that is not the server.
`RpcInvokePermission.Owner` - This RPC throws an exception if invoked by a game client that does not own the object.
`RpcInvokePermission.Everyone` - This can be invoked by any connected game client.
Default: `RpcInvokePermission.Everyone` | | `DeferLocal` | If `true`, RPCs that execute locally will be deferred until the start of the next frame, as if they had been sent over the network. (They will not actually be sent over the network, but will be treated as if they were.) This is useful for mutually recursive RPCs on hosts, where sending back and forth between the server and the "host client" will cause a stack overflow if each RPC is executed instantly; simulating the flow of RPCs between remote client and server enables this flow to work the same in both contexts.

Default: `false` | | `AllowTargetOverride` | By default, any `SendTo` value other than `SendTo.SpecifiedInParams` is a hard-coded value that cannot be changed. Setting this to `true` allows you to provide an alternate target at runtime, while using the `SendTo` value as a fallback if no runtime value is provided. | From a8237233403f26c18b2b3cad7a1024fcf4aada6a Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 23 Oct 2025 18:54:48 -0400 Subject: [PATCH 27/27] Add invocation order documentation --- .../advanced-topics/message-system/rpc.md | 110 +++++++++++ .../Runtime/Messaging/RpcParams.cs | 22 ++- .../Tests/Runtime/Rpc/RpcInvocationTests.cs | 179 ++++++++++++++++-- 3 files changed, 285 insertions(+), 26 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md index 56ea902ae9..2c7d915d06 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md @@ -215,6 +215,116 @@ There are a few other parameters that can be passed to either the `Rpc` attribut | `Target` | Runtime override destination for the RPC. (See above for more details.) Populating this value will throw an exception unless either the `SendTo` value for the RPC is `SendTo.SpecifiedInParams`, or `AllowTargetOverride` is `true`.

Default: `null` | | `LocalDeferMode` | Overrides the `DeferLocal` value. `DeferLocalMode.Defer` causes this particular invocation of this RPC to be deferred until the next frame even if `DeferLocal` is `false`, while `DeferLocalMode.SendImmediate` causes the RPC to be executed immediately on the local machine even if `DeferLocal` is `true`. `DeferLocalMode.Default` does whatever the `DeferLocal` value in the attribute is configured to do.

Options: `DeferLocalMode.Default`, `DeferLocalMode.Defer`, `DeferLocalMode.SendImmediate`
Default: `DeferLocalMode.Default` | +## Invocation order + +Rpc message sent with `RpcDelivery.Reliable` will be sent and invoked on other game clients in the same order as they were called on the local game client. + +```csharp +[Rpc(SendTo.Server)] +public void OpenDoorRpc(int doorId, RpcParams rpcParams) +{ + Debug.Log($"client {rpcParams.Receive.SenderClientId} has opened door {doorId}"); + + // Server can handle door opening here +} + +[Rpc(SendTo.Server)] +void OpenChestRPC(int chestId, RpcParams rpcParams) +{ + Debug.Log($"client {rpcParams.Receive.SenderClientId} has opened chest {chestId}"); + + // Server can handle door opening here +} + +void Update() +{ + if (IsClient && Input.GetKeyDown(KeyCode.O)) + { + OpenDoorRpc(1) + OpenDoorRpc(2) + OpenChestRpc(5) + } + + // Other clients will log: + // + // "client 1 has opened door 1" + // "client 1 has opened door 2" + // "client 1 has opened chest 5" +} +``` + +> [!Warning] +> Invocation order is not guaranteed with nested RPC invocations that include targets that may invoke locally. Invocation order is also not guaranteed when using `RpcDelivery.Unreliable` + +### Deferring local invocation + +Invoking an RPC from within another RPC introduces the risk that the local RPC may invoke before messages are sent to other game clients. This will result in the RPC message for the inner RPC invocation being sent before the message for the outer RPC. + +```csharp +[Rpc(SendTo.Everyone)] +public void TryOpenDoorRpc(int doorId, RpcParams rpcParams) +{ + Debug.Log($"client {rpcParams.Receive.SenderClientId} is trying to open door {doorId}"); + + if (HasAuthority) { + // Authority handles opening the door here + + // If the authority is invoking TryOpenDoorRpc locally before the authority has sent TryOpenDoorRpc to other clients, OpenDoorRpc will be sent before TryOpenDoorRpc. + OpenDoorRpc(doorId); + } +} + +[Rpc(SendTo.Everyone)] +public void OpenDoorRpc(int doorId, RpcParams rpcParams) +{ + Debug.Log($"client {rpcParams.Receive.SenderClientId} marked door {doorId} as open"); +} + +void Update() +{ + if (Input.GetKeyDown(KeyCode.O)) + { + // Invocation of TryOpenDoorRpc and OpenDoorRpc may be inverted depending on the context in which TryOpenDoorRpc is invoked + TryOpenDoorRpc(20); + } +} +``` + +Use the RPC `LocalDeferMode` to resolve issue. Configuring the RPC to be deferred when invoked locally will ensure that any outer RPC messages are always sent before the inner function is invoked. + +```csharp +// An RPC can be configured to defer the local invocation in the attribute definition +[Rpc(SendTo.Everyone, DeferLocal = true)] +public void TryOpenDoorRpc(int doorId, RpcParams rpcParams = default) +{ + Debug.Log($"client {rpcParams.Receive.SenderClientId} is trying to open door {doorId}"); + + if (HasAuthority) { + + // Authority handles opening the door here + + // Defer mode can also be passed in at the call site. + OpenDoorRpc(doorId, LocalDeferMode.Defer); + } +} + +[Rpc(SendTo.Everyone)] +public void OpenDoorRpc(int doorId, RpcParams rpcParams = default) +{ + Debug.Log($"client {rpcParams.Receive.SenderClientId} marked door {doorId} as open"); +} + +void Update() +{ + if (Input.GetKeyDown(KeyCode.O)) + { + // TryOpenDoorRpc is defined with DeferLocal + // DeferLocal ensures that RPC messages are sent to all other targets before the RPC is invoked locally + TryOpenDoorRpc(20); + } +} +``` + ## Additional resources * [RPC parameters](rpc-params.md) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcParams.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcParams.cs index 193dc64d2d..3323941803 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcParams.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcParams.cs @@ -9,8 +9,12 @@ namespace Unity.Netcode public enum LocalDeferMode { /// - /// Uses the default behavior for RPC message handling + /// Uses the default behavior for RPC message handling. + /// The default behavior is . /// + /// + /// If is enabled, the behavior of this field wil change to . + /// Default, /// @@ -25,7 +29,7 @@ public enum LocalDeferMode } /// - /// Generic RPC. Defines parameters for sending Remote Procedure Calls (RPCs) in the network system + /// Defines parameters for sending Remote Procedure Calls (RPCs) in the network system /// public struct RpcSendParams { @@ -55,31 +59,29 @@ public struct RpcSendParams } /// - /// The receive parameters for server-side remote procedure calls + /// The receive parameters for an RPC call /// public struct RpcReceiveParams { /// - /// Server-Side RPC - /// The client identifier of the sender + /// The sender's client identifier /// public ulong SenderClientId; } /// - /// Server-Side RPC - /// Can be used with any sever-side remote procedure call - /// Note: typically this is use primarily for the + /// Parameters for an RPC call + /// Can be used with any remote procedure call /// public struct RpcParams { /// - /// The server RPC send parameters (currently a place holder) + /// The RPC send parameters (currently a place holder) /// public RpcSendParams Send; /// - /// The client RPC receive parameters provides you with the sender's identifier + /// The RPC receive parameters provides you with the sender's identifier /// public RpcReceiveParams Receive; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs index 100faba4d0..ba9708da9b 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs @@ -60,7 +60,6 @@ private bool AllExpectedCallsReceived(StringBuilder errorLog) public IEnumerator RpcInvokePermissionSendingTests() { var nonAuthority = GetNonAuthorityNetworkManager(); - var authorityInstance = SpawnObject(m_Prefab, nonAuthority).GetComponent(); yield return WaitForSpawnedOnAllOrTimeOut(authorityInstance); @@ -88,17 +87,15 @@ public IEnumerator RpcInvokePermissionSendingTests() threwException = true; } - if (!manager.IsServer) - { - Assert.IsTrue(threwException); - } + // Server should not throw, everyone else should throw + Assert.AreEqual(!manager.IsServer, threwException, $"[Client-{manager.LocalClientId}] had an unexpected exception behaviour. Expected {(manager.IsServer ? "no exception" : "exception")} but was {(threwException ? "exception" : "no exception")}"); } yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); AssertOnTimeout("[InvokePermissions.Server] Rpc invoked an incorrect number of times"); // [Rpc(SendTo.Everyone, InvokePermission.Owner)] - foreach (var (_, instance) in m_InvokeInstances) + foreach (var (manager, instance) in m_InvokeInstances) { instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.OwnerInvokePermissionRpc)] = 1; @@ -113,10 +110,7 @@ public IEnumerator RpcInvokePermissionSendingTests() threwException = true; } - if (!instance.IsOwner) - { - Assert.IsTrue(threwException); - } + Assert.AreEqual(!instance.IsOwner, threwException, $"[Client-{manager.LocalClientId}] had an unexpected exception behaviour. Expected {(instance.IsOwner ? "no exception" : "exception")} but was {(threwException ? "exception" : "no exception")}"); } yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); @@ -225,6 +219,122 @@ public IEnumerator RpcInvokePermissionReceivingTests() Assert.That(firstClientInstance.SenderIdReceived, Is.EqualTo(secondClient.LocalClientId), "Received spoofed sender id!"); } + private bool ValidateInvocationOrder(StringBuilder errorLog) + { + var allInstancesValid = true; + foreach (var instance in m_InvokeInstances.Values) + { + if (!instance.RpcsWereInvokedInExpectedOrder(errorLog)) + { + allInstancesValid = false; + } + } + return allInstancesValid; + } + + [UnityTest] + public IEnumerator RpcInvocationOrderTests() + { + var authority = GetAuthorityNetworkManager(); + var authorityInstance = SpawnObject(m_Prefab, authority).GetComponent(); + var errorLog = new StringBuilder(); + + yield return WaitForSpawnedOnAllOrTimeOut(authorityInstance.NetworkObjectId); + AssertOnTimeout("Failed to spawn InvokePermissions test object"); + + Assert.IsTrue(authorityInstance.IsOwner); + + BuildInvokeInstancesMap(authorityInstance.NetworkObjectId); + + var expectedOrder = new List() + { + nameof(InvokePermissionBehaviour.EveryoneInvokePermissionRpc), + nameof(InvokePermissionBehaviour.OwnerInvokePermissionRpc), + nameof(InvokePermissionBehaviour.AnotherEveryoneInvokePermissionRpc), + }; + foreach (var instance in m_InvokeInstances.Values) + { + instance.ExpectedInvocationOrder = expectedOrder; + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.EveryoneInvokePermissionRpc)] = 1; + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.OwnerInvokePermissionRpc)] = 1; + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.AnotherEveryoneInvokePermissionRpc)] = 1; + } + + authorityInstance.EveryoneInvokePermissionRpc(); + authorityInstance.OwnerInvokePermissionRpc(); + authorityInstance.AnotherEveryoneInvokePermissionRpc(); + + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout("[Simple ordering][authority] Incorrect number of rpcs were invoked"); + Assert.IsTrue(ValidateInvocationOrder(errorLog), $"[Simple ordering][authority] Rpcs were invoked in an incorrect order\n {errorLog}"); + errorLog.Clear(); + + ResetAllExpectedInvocations(); + + var nonAuthority = GetNonAuthorityNetworkManager(); + var nonAuthorityInstance = m_InvokeInstances[nonAuthority]; + + expectedOrder = new List() + { + nameof(InvokePermissionBehaviour.AnotherEveryoneInvokePermissionRpc), + nameof(InvokePermissionBehaviour.EveryoneInvokePermissionRpc), + nameof(InvokePermissionBehaviour.AnotherEveryoneInvokePermissionRpc), + }; + foreach (var instance in m_InvokeInstances.Values) + { + instance.ExpectedInvocationOrder = expectedOrder; + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.EveryoneInvokePermissionRpc)] = 1; + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.AnotherEveryoneInvokePermissionRpc)] = 2; + } + + nonAuthorityInstance.AnotherEveryoneInvokePermissionRpc(); + nonAuthorityInstance.EveryoneInvokePermissionRpc(); + nonAuthorityInstance.AnotherEveryoneInvokePermissionRpc(); + + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout("[Simple ordering][nonAuthority] Incorrect number of rpcs were invoked"); + Assert.IsTrue(ValidateInvocationOrder(errorLog), $"[Simple ordering][nonAuthority] Rpcs were invoked in an incorrect order\n {errorLog}"); + errorLog.Clear(); + + for (var i = 0; i < 3; i++) + { + var testType = (LocalDeferMode)i; + + ResetAllExpectedInvocations(); + + expectedOrder = new List() + { + nameof(InvokePermissionBehaviour.NestedInvocationRpc), + nameof(InvokePermissionBehaviour.EveryoneInvokePermissionRpc), + }; + var reversedOrder = new List() { expectedOrder[1], expectedOrder[0] }; + foreach (var (manager, instance) in m_InvokeInstances) + { + // Invocation order will be reversed when not the invoking instance if not using defer mode + var isReversed = testType != LocalDeferMode.Defer && manager != nonAuthority; + instance.ExpectedInvocationOrder = isReversed ? reversedOrder : expectedOrder; + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.NestedInvocationRpc)] = 1; + instance.ExpectedCallCounts[nameof(InvokePermissionBehaviour.EveryoneInvokePermissionRpc)] = 1; + } + + nonAuthorityInstance.NestedInvocationRpc(testType); + + yield return WaitForConditionOrTimeOut(AllExpectedCallsReceived); + AssertOnTimeout($"[Has nested][nonAuthority][{testType}] Incorrect number of rpcs were invoked"); + Assert.IsTrue(ValidateInvocationOrder(errorLog), $"[Has nested][nonAuthority][{testType}] Rpcs were invoked in an incorrect order\n {errorLog}"); + errorLog.Clear(); + } + } + + private void ResetAllExpectedInvocations() + { + foreach (var instance in m_InvokeInstances.Values) + { + instance.Reset(); + } + } + + private void SendUncheckedMessage(NetworkManager manager, InvokePermissionBehaviour invokePermissionsObject, string rpcMethodName) { using var bufferWriter = new FastBufferWriter(1024, Allocator.Temp); @@ -270,8 +380,10 @@ private uint GetMethodIdFromMethodName(string methodName) internal class InvokePermissionBehaviour : NetworkBehaviour { - public readonly Dictionary RpcCallCounts = new(); + private readonly Dictionary m_RpcCallCounts = new(); public readonly Dictionary ExpectedCallCounts = new(); + private readonly List m_RpcInvocationOrder = new(); + public List ExpectedInvocationOrder = new(); public bool HasReceivedExpectedRpcs(StringBuilder errorLog) { @@ -280,7 +392,7 @@ public bool HasReceivedExpectedRpcs(StringBuilder errorLog) foreach (var (expectedMethodCall, expectedCallCount) in ExpectedCallCounts) { seen.Add(expectedMethodCall); - if (!RpcCallCounts.TryGetValue(expectedMethodCall, out var actualCallCount)) + if (!m_RpcCallCounts.TryGetValue(expectedMethodCall, out var actualCallCount)) { errorLog.AppendLine($"[Client-{NetworkManager.LocalClientId}] Expected {expectedMethodCall} to have been invoked!"); } @@ -293,7 +405,7 @@ public bool HasReceivedExpectedRpcs(StringBuilder errorLog) } // Ensure no other rpcs were called when they weren't expected - foreach (var rpcCall in RpcCallCounts.Keys) + foreach (var rpcCall in m_RpcCallCounts.Keys) { if (!seen.Contains(rpcCall)) { @@ -305,10 +417,26 @@ public bool HasReceivedExpectedRpcs(StringBuilder errorLog) return isValid; } + public bool RpcsWereInvokedInExpectedOrder(StringBuilder errorLog) + { + var isValid = true; + for (var i = 0; i < m_RpcInvocationOrder.Count; i++) + { + if (!ExpectedInvocationOrder[i].Equals(m_RpcInvocationOrder[i])) + { + errorLog.AppendLine($"[Client-{NetworkManager.LocalClientId}][Invocation-{i}] Rpc invoked in incorrect order. Expected {ExpectedInvocationOrder[i]}, but was {m_RpcInvocationOrder[i]}"); + isValid = false; + } + } + return isValid; + } + public void Reset() { - RpcCallCounts.Clear(); + m_RpcCallCounts.Clear(); ExpectedCallCounts.Clear(); + m_RpcInvocationOrder.Clear(); + ExpectedInvocationOrder.Clear(); } [Rpc(SendTo.Everyone, InvokePermission = RpcInvokePermission.Server)] @@ -329,6 +457,23 @@ public void EveryoneInvokePermissionRpc() TrackRpcCalled(GetCaller()); } + [Rpc(SendTo.Everyone, InvokePermission = RpcInvokePermission.Everyone)] + public void AnotherEveryoneInvokePermissionRpc() + { + TrackRpcCalled(GetCaller()); + } + + [Rpc(SendTo.Everyone)] + public void NestedInvocationRpc(RpcParams rpcParams = default) + { + TrackRpcCalled(GetCaller()); + + if (rpcParams.Receive.SenderClientId == NetworkManager.LocalClientId) + { + EveryoneInvokePermissionRpc(); + } + } + internal ulong SenderIdReceived; [Rpc(SendTo.Owner)] public void TrackSenderIdRpc(RpcParams rpcParams) @@ -340,11 +485,13 @@ public void TrackSenderIdRpc(RpcParams rpcParams) private void TrackRpcCalled(string rpcName) { // TryAdd returns false and will not add anything if the key already existed. - if (!RpcCallCounts.TryAdd(rpcName, 1)) + if (!m_RpcCallCounts.TryAdd(rpcName, 1)) { // If the key already existed, increment it - RpcCallCounts[rpcName]++; + m_RpcCallCounts[rpcName]++; } + + m_RpcInvocationOrder.Add(rpcName); } private static string GetCaller([CallerMemberName] string caller = null)