RemoteCalls.cs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. using System;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. namespace Mirror.RemoteCalls
  5. {
  6. // invoke type for Cmd/Rpc
  7. public enum RemoteCallType { Command, ClientRpc }
  8. // remote call function delegate
  9. public delegate void RemoteCallDelegate(NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection);
  10. class Invoker
  11. {
  12. // GameObjects might have multiple components of TypeA.CommandA().
  13. // when invoking, we check if 'TypeA' is an instance of the type.
  14. // the hash itself isn't enough because we wouldn't know which component
  15. // to invoke it on if there are multiple of the same type.
  16. public Type componentType;
  17. public RemoteCallType callType;
  18. public RemoteCallDelegate function;
  19. public bool cmdRequiresAuthority;
  20. public bool AreEqual(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate invokeFunction) =>
  21. this.componentType == componentType &&
  22. this.callType == remoteCallType &&
  23. this.function == invokeFunction;
  24. }
  25. /// <summary>Used to help manage remote calls for NetworkBehaviours</summary>
  26. public static class RemoteProcedureCalls
  27. {
  28. // one lookup for all remote calls.
  29. // allows us to easily add more remote call types without duplicating code.
  30. // note: do not clear those with [RuntimeInitializeOnLoad]
  31. //
  32. // IMPORTANT: cmd/rpc functions are identified via **HASHES**.
  33. // an index would requires half the bandwidth, but introduces issues
  34. // where static constructors are lazily called, so index order isn't
  35. // guaranteed. keep hashes to avoid:
  36. // https://github.com/vis2k/Mirror/pull/3135
  37. // https://github.com/vis2k/Mirror/issues/3138
  38. // BUT: 2 byte hash is enough if we check for collisions. that's what we
  39. // do for NetworkMessage as well.
  40. static readonly Dictionary<ushort, Invoker> remoteCallDelegates = new Dictionary<ushort, Invoker>();
  41. static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, ushort functionHash)
  42. {
  43. if (remoteCallDelegates.ContainsKey(functionHash))
  44. {
  45. // something already registered this hash.
  46. // it's okay if it was the same function.
  47. Invoker oldInvoker = remoteCallDelegates[functionHash];
  48. if (oldInvoker.AreEqual(componentType, remoteCallType, func))
  49. {
  50. return true;
  51. }
  52. // otherwise notify user. there is a rare chance of string
  53. // hash collisions.
  54. Debug.LogError($"Function {oldInvoker.componentType}.{oldInvoker.function.GetMethodName()} and {componentType}.{func.GetMethodName()} have the same hash. Please rename one of them. To save bandwidth, we only use 2 bytes for the hash, which has a small chance of collisions.");
  55. }
  56. return false;
  57. }
  58. // pass full function name to avoid ClassA.Func & ClassB.Func collisions
  59. internal static ushort RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true)
  60. {
  61. // type+func so Inventory.RpcUse != Equipment.RpcUse
  62. ushort hash = (ushort)(functionFullName.GetStableHashCode() & 0xFFFF);
  63. if (CheckIfDelegateExists(componentType, remoteCallType, func, hash))
  64. return hash;
  65. remoteCallDelegates[hash] = new Invoker
  66. {
  67. callType = remoteCallType,
  68. componentType = componentType,
  69. function = func,
  70. cmdRequiresAuthority = cmdRequiresAuthority
  71. };
  72. return hash;
  73. }
  74. // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
  75. // need to pass componentType to support invoking on GameObjects with
  76. // multiple components of same type with same remote call.
  77. public static void RegisterCommand(Type componentType, string functionFullName, RemoteCallDelegate func, bool requiresAuthority) =>
  78. RegisterDelegate(componentType, functionFullName, RemoteCallType.Command, func, requiresAuthority);
  79. // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
  80. // need to pass componentType to support invoking on GameObjects with
  81. // multiple components of same type with same remote call.
  82. public static void RegisterRpc(Type componentType, string functionFullName, RemoteCallDelegate func) =>
  83. RegisterDelegate(componentType, functionFullName, RemoteCallType.ClientRpc, func);
  84. // to clean up tests
  85. internal static void RemoveDelegate(ushort hash) =>
  86. remoteCallDelegates.Remove(hash);
  87. // note: no need to throw an error if not found.
  88. // an attacker might just try to call a cmd with an rpc's hash etc.
  89. // returning false is enough.
  90. static bool GetInvokerForHash(ushort functionHash, RemoteCallType remoteCallType, out Invoker invoker) =>
  91. remoteCallDelegates.TryGetValue(functionHash, out invoker) &&
  92. invoker != null &&
  93. invoker.callType == remoteCallType;
  94. // InvokeCmd/Rpc Delegate can all use the same function here
  95. internal static bool Invoke(ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null)
  96. {
  97. // IMPORTANT: we check if the message's componentIndex component is
  98. // actually of the right type. prevents attackers trying
  99. // to invoke remote calls on wrong components.
  100. if (GetInvokerForHash(functionHash, remoteCallType, out Invoker invoker) &&
  101. invoker.componentType.IsInstanceOfType(component))
  102. {
  103. // invoke function on this component
  104. invoker.function(component, reader, senderConnection);
  105. return true;
  106. }
  107. return false;
  108. }
  109. // check if the command 'requiresAuthority' which is set in the attribute
  110. internal static bool CommandRequiresAuthority(ushort cmdHash) =>
  111. GetInvokerForHash(cmdHash, RemoteCallType.Command, out Invoker invoker) &&
  112. invoker.cmdRequiresAuthority;
  113. /// <summary>Gets the handler function by hash. Useful for profilers and debuggers.</summary>
  114. public static RemoteCallDelegate GetDelegate(ushort functionHash) =>
  115. remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker)
  116. ? invoker.function
  117. : null;
  118. }
  119. }