using System; using System.Collections.Generic; using UnityEngine; namespace Mirror.RemoteCalls { // invoke type for Cmd/Rpc public enum RemoteCallType { Command, ClientRpc } // remote call function delegate public delegate void RemoteCallDelegate(NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection); class Invoker { // GameObjects might have multiple components of TypeA.CommandA(). // when invoking, we check if 'TypeA' is an instance of the type. // the hash itself isn't enough because we wouldn't know which component // to invoke it on if there are multiple of the same type. public Type componentType; public RemoteCallType callType; public RemoteCallDelegate function; public bool cmdRequiresAuthority; public bool AreEqual(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate invokeFunction) => this.componentType == componentType && this.callType == remoteCallType && this.function == invokeFunction; } /// Used to help manage remote calls for NetworkBehaviours public static class RemoteProcedureCalls { public const string InvokeRpcPrefix = "InvokeUserCode_"; // one lookup for all remote calls. // allows us to easily add more remote call types without duplicating code. // note: do not clear those with [RuntimeInitializeOnLoad] // // IMPORTANT: cmd/rpc functions are identified via **HASHES**. // an index would requires half the bandwidth, but introduces issues // where static constructors are lazily called, so index order isn't // guaranteed. keep hashes to avoid: // https://github.com/vis2k/Mirror/pull/3135 // https://github.com/vis2k/Mirror/issues/3138 // BUT: 2 byte hash is enough if we check for collisions. that's what we // do for NetworkMessage as well. static readonly Dictionary remoteCallDelegates = new Dictionary(); static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, ushort functionHash) { if (remoteCallDelegates.ContainsKey(functionHash)) { // something already registered this hash. // it's okay if it was the same function. Invoker oldInvoker = remoteCallDelegates[functionHash]; if (oldInvoker.AreEqual(componentType, remoteCallType, func)) { return true; } // otherwise notify user. there is a rare chance of string // hash collisions. 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."); } return false; } // pass full function name to avoid ClassA.Func & ClassB.Func collisions internal static ushort RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true) { // type+func so Inventory.RpcUse != Equipment.RpcUse ushort hash = (ushort)(functionFullName.GetStableHashCode() & 0xFFFF); if (CheckIfDelegateExists(componentType, remoteCallType, func, hash)) return hash; remoteCallDelegates[hash] = new Invoker { callType = remoteCallType, componentType = componentType, function = func, cmdRequiresAuthority = cmdRequiresAuthority }; return hash; } // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions // need to pass componentType to support invoking on GameObjects with // multiple components of same type with same remote call. public static void RegisterCommand(Type componentType, string functionFullName, RemoteCallDelegate func, bool requiresAuthority) => RegisterDelegate(componentType, functionFullName, RemoteCallType.Command, func, requiresAuthority); // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions // need to pass componentType to support invoking on GameObjects with // multiple components of same type with same remote call. public static void RegisterRpc(Type componentType, string functionFullName, RemoteCallDelegate func) => RegisterDelegate(componentType, functionFullName, RemoteCallType.ClientRpc, func); // to clean up tests internal static void RemoveDelegate(ushort hash) => remoteCallDelegates.Remove(hash); internal static bool GetFunctionMethodName(ushort functionHash, out string methodName) { if (remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker)) { methodName = invoker.function.GetMethodName().Replace(InvokeRpcPrefix, ""); return true; } methodName = ""; return false; } // note: no need to throw an error if not found. // an attacker might just try to call a cmd with an rpc's hash etc. // returning false is enough. static bool GetInvokerForHash(ushort functionHash, RemoteCallType remoteCallType, out Invoker invoker) => remoteCallDelegates.TryGetValue(functionHash, out invoker) && invoker != null && invoker.callType == remoteCallType; // InvokeCmd/Rpc Delegate can all use the same function here internal static bool Invoke(ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null) { // IMPORTANT: we check if the message's componentIndex component is // actually of the right type. prevents attackers trying // to invoke remote calls on wrong components. if (GetInvokerForHash(functionHash, remoteCallType, out Invoker invoker) && invoker.componentType.IsInstanceOfType(component)) { // invoke function on this component invoker.function(component, reader, senderConnection); return true; } return false; } // check if the command 'requiresAuthority' which is set in the attribute internal static bool CommandRequiresAuthority(ushort cmdHash) => GetInvokerForHash(cmdHash, RemoteCallType.Command, out Invoker invoker) && invoker.cmdRequiresAuthority; /// Gets the handler function by hash. Useful for profilers and debuggers. public static RemoteCallDelegate GetDelegate(ushort functionHash) => remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker) ? invoker.function : null; } }