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 { // 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] static readonly Dictionary remoteCallDelegates = new Dictionary(); static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, int 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"); } return false; } // pass full function name to avoid ClassA.Func & ClassB.Func collisions internal static int RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true) { // type+func so Inventory.RpcUse != Equipment.RpcUse int hash = functionFullName.GetStableHashCode(); 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(int hash) => remoteCallDelegates.Remove(hash); // 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(int 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(int 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(int 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(int functionHash) => remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker) ? invoker.function : null; } }