RemoteCalls.cs 7.19 KB
Newer Older
cann-alberto's avatar
cann-alberto committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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;
    }

    /// <summary>Used to help manage remote calls for NetworkBehaviours</summary>
    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<ushort, Invoker> remoteCallDelegates = new Dictionary<ushort, Invoker>();

        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;

        /// <summary>Gets the handler function by hash. Useful for profilers and debuggers.</summary>
        public static RemoteCallDelegate GetDelegate(ushort functionHash) =>
            remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker)
            ? invoker.function
            : null;
    }
}