diff --git a/Reactor.Example/CustomRpcs.cs b/Reactor.Example/CustomRpcs.cs index 5568438..975da54 100644 --- a/Reactor.Example/CustomRpcs.cs +++ b/Reactor.Example/CustomRpcs.cs @@ -2,6 +2,7 @@ namespace Reactor.Example { public enum CustomRpcCalls : uint { - Example + Example, + MethodRpcExample } } diff --git a/Reactor.Example/ExamplePlugin.cs b/Reactor.Example/ExamplePlugin.cs index 55e5714..63a1bc5 100644 --- a/Reactor.Example/ExamplePlugin.cs +++ b/Reactor.Example/ExamplePlugin.cs @@ -1,8 +1,12 @@ using System; using BepInEx; using BepInEx.IL2CPP; +using Reactor.Extensions; using Reactor.Networking; +using Reactor.Networking.MethodRpc; +using UnhollowerBaseLib.Attributes; using UnityEngine; +using Random = UnityEngine.Random; namespace Reactor.Example { @@ -25,17 +29,31 @@ namespace Reactor.Example private void Update() { - if (Input.GetKeyDown(KeyCode.F3) && AmongUsClient.Instance && PlayerControl.LocalPlayer) + if (AmongUsClient.Instance && PlayerControl.LocalPlayer) { - var name = PlayerControl.LocalPlayer.Data.PlayerName; - Rpc.Instance.Send(new ExampleRpc.Data($"Send: from {name}")); - - if (!AmongUsClient.Instance.AmHost) + if (Input.GetKeyDown(KeyCode.F3)) { - Rpc.Instance.SendTo(AmongUsClient.Instance.HostId, new ExampleRpc.Data($"SendTo: from {name} to host")); + var name = PlayerControl.LocalPlayer.Data.PlayerName; + Rpc.Instance.Send(new ExampleRpc.Data($"Send: from {name}")); + + if (!AmongUsClient.Instance.AmHost) + { + Rpc.Instance.SendTo(AmongUsClient.Instance.HostId, new ExampleRpc.Data($"SendTo: from {name} to host")); + } + } + + if (Input.GetKeyDown(KeyCode.F4)) + { + RpcSay(PlayerControl.LocalPlayer, "Hello from method rpc", Random.value, PlayerControl.LocalPlayer); } } } } + + [MethodRpc((uint) CustomRpcCalls.MethodRpcExample)] + public static void RpcSay(PlayerControl player, string text, float number, PlayerControl testPlayer) + { + Logger.Info($"{player.Data.PlayerName} text: {text} number: {number} testPlayer: {testPlayer.NetId}"); + } } } diff --git a/Reactor.Example/Reactor.Example.csproj b/Reactor.Example/Reactor.Example.csproj index ddd980b..ad82b9c 100644 --- a/Reactor.Example/Reactor.Example.csproj +++ b/Reactor.Example/Reactor.Example.csproj @@ -2,6 +2,7 @@ netstandard2.1 latest + embedded 0.1.0 Example mod using Reactor diff --git a/Reactor/Networking/MethodRpc/MethodRpc.cs b/Reactor/Networking/MethodRpc/MethodRpc.cs new file mode 100644 index 0000000..7206395 --- /dev/null +++ b/Reactor/Networking/MethodRpc/MethodRpc.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.InteropServices; +using BepInEx.IL2CPP; +using HarmonyLib; +using Hazel; +using InnerNet; +using MonoMod.RuntimeDetour; +using MonoMod.Utils; +using Reactor.Networking.Serialization; + +namespace Reactor.Networking.MethodRpc +{ + public class MethodRpc : UnsafeCustomRpc + { + public delegate object HandleDelegate(InnerNetObject innerNetObject, object[] args); + + public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption option, RpcLocalHandling localHandling) : base(plugin, id) + { + Method = method; + LocalHandling = localHandling; + SendOption = option; + + if (!method.IsStatic) + { + throw new NotImplementedException("Instance method rpc support is postponed until unhollower v0.5.0"); + } + + var parameters = method.GetParameters(); + + if (method.IsStatic && parameters.Length == 0) + { + throw new ArgumentException("Static method rpc requires at least one argument", nameof(method)); + } + + var innerNetObjectType = parameters.First().ParameterType; + + if (!typeof(InnerNetObject).IsAssignableFrom(innerNetObjectType)) + { + throw new ArgumentException("First argument of a static method rpc has to be an InnerNetObject", nameof(method)); + } + + InnerNetObjectType = innerNetObjectType; + + Handle = Hook(method, parameters); + } + + public MethodInfo Method { get; } + public HandleDelegate Handle { get; } + + public override Type InnerNetObjectType { get; } + public override RpcLocalHandling LocalHandling { get; } + public override SendOption SendOption { get; } + + public override void UnsafeWrite(MessageWriter writer, object data) + { + var args = (object[]) data; + MessageSerializer.Serialize(writer, args); + } + + public override object UnsafeRead(MessageReader reader) + { + var args = MessageSerializer.Deserialize(reader, this); + return args; + } + + public override void UnsafeHandle(InnerNetObject innerNetObject, object data) + { + var args = (object[]) data; + var result = Handle(innerNetObject, args); + + if (result is IEnumerator enumerator) + { + Coroutines.Start(enumerator); + } + } + + public void Send(InnerNetObject innerNetObject, object[] args) + { + UnsafeSend(innerNetObject, args); + } + + private static readonly MethodInfo _sendMethod = AccessTools.Method(typeof(MethodRpc), nameof(Send)); + + /// + /// Hooks the rpc with a dynamic method that sends it + /// + private HandleDelegate Hook(MethodInfo method, ParameterInfo[] parameters) + { + var detour = new Detour(method, GenerateSender()); + + // Used as target when hooking, sends the method rpc + DynamicMethod GenerateSender() + { + var dynamicMethod = new DynamicMethod($"Sender<{method.GetID(simple: true)}>", method.ReturnType, parameters.Select(x => x.ParameterType).ToArray()); + + foreach (var parameter in parameters) + { + dynamicMethod.DefineParameter(parameter.Position, ParameterAttributes.None, parameter.Name); + } + + var il = dynamicMethod.GetILGenerator(); + + // :yeefuckinhaw: + // Black magic from stackoverflow, they also said that you shouldn't do that ;) + // Resort to Dictionary if this turns out to be unreliable + { + var handle = GCHandle.Alloc(this); + var ptr = GCHandle.ToIntPtr(handle); + + if (IntPtr.Size == 4) + { + il.Emit(OpCodes.Ldc_I4, ptr.ToInt32()); + } + else + { + il.Emit(OpCodes.Ldc_I8, ptr.ToInt64()); + } + + il.Emit(OpCodes.Conv_I); + + // TODO Figure out why this always throws NullReferenceException on mono + // il.Emit(OpCodes.Ldobj, typeof(MethodRpc)); + + // Workaround for ^, speed seems to be the same + il.Emit(OpCodes.Call, AccessTools.Method(typeof(GCHandle), nameof(GCHandle.FromIntPtr))); + var temp = il.DeclareLocal(typeof(GCHandle)); + il.Emit(OpCodes.Stloc, temp); + il.Emit(OpCodes.Ldloca, temp); + il.Emit(OpCodes.Call, AccessTools.PropertyGetter(typeof(GCHandle), nameof(GCHandle.Target))); + } + + il.Emit(OpCodes.Ldarg_0); + + il.Emit(OpCodes.Ldc_I4, parameters.Length - 1); + il.Emit(OpCodes.Newarr, typeof(object)); + + for (var i = 1; i < parameters.Length; i++) + { + var parameter = parameters[i]; + + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldc_I4, i - 1); + il.Emit(OpCodes.Ldarg, i); + + var parameterType = parameter.ParameterType; + if (parameterType.IsValueType) + { + il.Emit(OpCodes.Box, parameterType); + } + + il.Emit(OpCodes.Stelem_Ref); + } + + il.Emit(OpCodes.Call, _sendMethod); + + if (method.ReturnType != typeof(void)) + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Ret); + + return dynamicMethod; + } + + // Proxy translating object array to trampoline method args, used by MethodRpc to invoke original handling + HandleDelegate GenerateHandler(DynamicMethod trampoline) + { + var dynamicMethod = new DynamicMethod($"Handler<{method.GetID(simple: true)}>", typeof(object), new[] { typeof(InnerNetObject), typeof(object[]) }); + dynamicMethod.DefineParameter(0, ParameterAttributes.None, "innerNetObject"); + dynamicMethod.DefineParameter(1, ParameterAttributes.None, "args"); + + var il = dynamicMethod.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, InnerNetObjectType); + + for (var i = 1; i < parameters.Length; i++) + { + var parameter = parameters[i]; + + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldc_I4, i - 1); + il.Emit(OpCodes.Ldelem_Ref); + + var parameterType = parameter.ParameterType; + il.Emit(parameterType.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, parameterType); + } + + il.Emit(OpCodes.Call, trampoline); + + if (method.ReturnType == typeof(void)) + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Ret); + + return dynamicMethod.CreateDelegate(); + } + + return GenerateHandler((DynamicMethod) detour.GenerateTrampoline()); + } + } +} diff --git a/Reactor/Networking/MethodRpc/MethodRpcAttribute.cs b/Reactor/Networking/MethodRpc/MethodRpcAttribute.cs new file mode 100644 index 0000000..e9118d9 --- /dev/null +++ b/Reactor/Networking/MethodRpc/MethodRpcAttribute.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Reflection; +using BepInEx.IL2CPP; +using HarmonyLib; +using Hazel; + +namespace Reactor.Networking.MethodRpc +{ + [AttributeUsage(AttributeTargets.Method)] + public class MethodRpcAttribute : Attribute + { + public uint Id { get; } + public SendOption Option { get; set; } = SendOption.Reliable; + public RpcLocalHandling LocalHandling { get; set; } = RpcLocalHandling.Before; + + public MethodRpcAttribute(uint id) + { + Id = id; + } + + public static void Register(Assembly assembly, BasePlugin plugin) + { + var methods = assembly.GetTypes() + .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)); + + foreach (var method in methods) + { + var attribute = method.GetCustomAttribute(); + if (attribute == null) + { + continue; + } + + try + { + var customRpc = new MethodRpc(plugin, method, attribute.Id, attribute.Option, attribute.LocalHandling); + PluginSingleton.Instance.CustomRpcManager.Register(customRpc); + } + catch (Exception e) + { + Logger.Warning($"Failed to register {method.FullDescription()}: {e}"); + } + } + } + + internal static void Initialize() + { + ChainloaderHooks.PluginLoad += plugin => Register(plugin.GetType().Assembly, plugin); + } + } +} diff --git a/Reactor/Networking/Serialization/InnerNetObjectConverter.cs b/Reactor/Networking/Serialization/InnerNetObjectConverter.cs new file mode 100644 index 0000000..3effd6f --- /dev/null +++ b/Reactor/Networking/Serialization/InnerNetObjectConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using Hazel; +using InnerNet; +using MonoMod.Utils; + +namespace Reactor.Networking.Serialization +{ + [MessageConverter] + public class InnerNetObjectConverter : MessageConverter + { + private static readonly MethodInfo _readNetObject = AccessTools.Method(typeof(MessageExtensions), nameof(MessageExtensions.ReadNetObject)); + private static readonly Dictionary> _readNetObjectMap = new Dictionary>(); + + public static InnerNetObject ReadNetObject(MessageReader reader, Type t) + { + if (_readNetObjectMap.TryGetValue(t, out var value)) + { + return value(reader); + } + + var @delegate = _readNetObject.MakeGenericMethod(t).CreateDelegate>(); + _readNetObjectMap[t] = @delegate; + return @delegate(reader); + } + + public override void Write(MessageWriter writer, InnerNetObject value) + { + MessageExtensions.WriteNetObject(writer, value); + } + + public override InnerNetObject Read(MessageReader reader, Type objectType) + { + return ReadNetObject(reader, objectType); + } + } +} diff --git a/Reactor/Networking/Serialization/MessageConverter.cs b/Reactor/Networking/Serialization/MessageConverter.cs new file mode 100644 index 0000000..5e7465c --- /dev/null +++ b/Reactor/Networking/Serialization/MessageConverter.cs @@ -0,0 +1,29 @@ +using System; +using Hazel; + +namespace Reactor.Networking.Serialization +{ + public abstract class UnsafeMessageConverter + { + public abstract void UnsafeWrite(MessageWriter writer, object value); + public abstract object UnsafeRead(MessageReader reader, Type objectType); + + public abstract bool CanConvert(Type objectType); + } + + public abstract class MessageConverter : UnsafeMessageConverter + { + public abstract void Write(MessageWriter writer, T value); + + public abstract T Read(MessageReader reader, Type objectType); + + public override bool CanConvert(Type objectType) + { + return typeof(T).IsAssignableFrom(objectType); + } + + public override void UnsafeWrite(MessageWriter writer, object value) => Write(writer, (T) value); + + public override object UnsafeRead(MessageReader reader, Type objectType) => Read(reader, objectType); + } +} diff --git a/Reactor/Networking/Serialization/MessageConverterAttribute.cs b/Reactor/Networking/Serialization/MessageConverterAttribute.cs new file mode 100644 index 0000000..aa743e8 --- /dev/null +++ b/Reactor/Networking/Serialization/MessageConverterAttribute.cs @@ -0,0 +1,41 @@ +using System; +using System.Reflection; +using HarmonyLib; + +namespace Reactor.Networking.Serialization +{ + [AttributeUsage(AttributeTargets.Class)] + public class MessageConverterAttribute : Attribute + { + public static void Register(Assembly assembly) + { + foreach (var type in assembly.GetTypes()) + { + var attribute = type.GetCustomAttribute(); + + if (attribute != null) + { + if (!type.IsSubclassOf(typeof(UnsafeMessageConverter))) + { + throw new InvalidOperationException($"Type {type.FullDescription()} has {nameof(MessageConverterAttribute)} but doesn't extend {nameof(UnsafeMessageConverter)}."); + } + + try + { + var messageConverter = (UnsafeMessageConverter) Activator.CreateInstance(type); + MessageSerializer.Register(messageConverter); + } + catch (Exception e) + { + Logger.Warning($"Failed to register {type.FullDescription()}: {e}"); + } + } + } + } + + internal static void Initialize() + { + ChainloaderHooks.PluginLoad += plugin => Register(plugin.GetType().Assembly); + } + } +} diff --git a/Reactor/Networking/Serialization/MessageSerializer.cs b/Reactor/Networking/Serialization/MessageSerializer.cs new file mode 100644 index 0000000..5f00f04 --- /dev/null +++ b/Reactor/Networking/Serialization/MessageSerializer.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Hazel; +using Reactor.Extensions; +using UnityEngine; + +namespace Reactor.Networking.Serialization +{ + public static class MessageSerializer + { + private static List MessageConverters { get; } = new List(); + + private static Dictionary MessageConverterMap { get; } = new Dictionary(); + + public static void Register(UnsafeMessageConverter messageConverter) + { + MessageConverters.Add(messageConverter); + MessageConverterMap.Clear(); + } + + public static UnsafeMessageConverter FindConverter(Type type) + { + if (MessageConverterMap.TryGetValue(type, out var value)) + { + return value; + } + + var converter = MessageConverters.SingleOrDefault(x => x.CanConvert(type)); + MessageConverterMap.Add(type, converter); + + return converter; + } + + public static void Serialize(MessageWriter writer, object[] args) + { + foreach (var arg in args) + { + writer.Serialize(arg); + } + } + + public static void Serialize(this MessageWriter writer, object o) + { + switch (o) + { + case int i: + writer.WritePacked(i); + break; + case uint i: + writer.WritePacked(i); + break; + case byte i: + writer.Write(i); + break; + case float i: + writer.Write(i); + break; + case sbyte i: + writer.Write(i); + break; + case ushort i: + writer.Write(i); + break; + case bool i: + writer.Write(i); + break; + case Vector2 i: + writer.Write(i); + break; + case string i: + writer.Write(i); + break; + default: + var converter = FindConverter(o.GetType()); + if (converter != null) + { + converter.UnsafeWrite(writer, o); + break; + } + + throw new NotSupportedException("Couldn't serialize " + o.GetType()); + } + } + + public static object[] Deserialize(MessageReader reader, MethodRpc.MethodRpc methodRpc) + { + var parameters = methodRpc.Method.GetParameters(); + var args = new object[parameters.Length - 1]; + + for (var i = 1; i < parameters.Length; i++) + { + var parameter = parameters[i]; + args[i - 1] = reader.Deserialize(parameter.ParameterType); + } + + return args; + } + + public static object Deserialize(this MessageReader reader, Type t) + { + if (t == typeof(int)) + { + return reader.ReadPackedInt32(); + } + + if (t == typeof(uint)) + { + return reader.ReadPackedUInt32(); + } + + if (t == typeof(byte)) + { + return reader.ReadByte(); + } + + if (t == typeof(float)) + { + return reader.ReadSingle(); + } + + if (t == typeof(sbyte)) + { + return reader.ReadSByte(); + } + + if (t == typeof(ushort)) + { + return reader.ReadUInt16(); + } + + if (t == typeof(bool)) + { + return reader.ReadBoolean(); + } + + if (t == typeof(Vector2)) + { + return reader.ReadVector2(); + } + + if (t == typeof(string)) + { + return reader.ReadString(); + } + + var converter = FindConverter(t); + if (converter != null) + { + return converter.UnsafeRead(reader, t); + } + + throw new NotSupportedException("Couldn't deserialize " + t); + } + } +} diff --git a/Reactor/Networking/UnsafeCustomRpc.cs b/Reactor/Networking/UnsafeCustomRpc.cs index 6a9f806..405a982 100644 --- a/Reactor/Networking/UnsafeCustomRpc.cs +++ b/Reactor/Networking/UnsafeCustomRpc.cs @@ -30,9 +30,9 @@ namespace Reactor.Networking public abstract object UnsafeRead(MessageReader reader); public abstract void UnsafeHandle(InnerNetObject innerNetObject, object data); - public void UnsafeSend(InnerNetObject netObject, object data, bool immediately = false, int targetClientId = -1) + public void UnsafeSend(InnerNetObject innerNetObject, object data, bool immediately = false, int targetClientId = -1) { - if (netObject == null) throw new ArgumentNullException(nameof(netObject)); + if (innerNetObject == null) throw new ArgumentNullException(nameof(innerNetObject)); if (Manager == null) { @@ -41,13 +41,13 @@ namespace Reactor.Networking if (LocalHandling == RpcLocalHandling.Before) { - UnsafeHandle(netObject, data); + UnsafeHandle(innerNetObject, data); } var writer = immediately switch { - false => AmongUsClient.Instance.StartRpc(netObject.NetId, CustomRpcManager.CallId, SendOption), - true => AmongUsClient.Instance.StartRpcImmediately(netObject.NetId, CustomRpcManager.CallId, SendOption, targetClientId) + false => AmongUsClient.Instance.StartRpc(innerNetObject.NetId, CustomRpcManager.CallId, SendOption), + true => AmongUsClient.Instance.StartRpcImmediately(innerNetObject.NetId, CustomRpcManager.CallId, SendOption, targetClientId) }; var pluginNetId = ModList.GetById(PluginId).NetId; @@ -69,7 +69,7 @@ namespace Reactor.Networking if (LocalHandling == RpcLocalHandling.After) { - UnsafeHandle(netObject, data); + UnsafeHandle(innerNetObject, data); } } } diff --git a/Reactor/ReactorPlugin.cs b/Reactor/ReactorPlugin.cs index c6907bb..91f58c9 100644 --- a/Reactor/ReactorPlugin.cs +++ b/Reactor/ReactorPlugin.cs @@ -5,6 +5,8 @@ using BepInEx.Configuration; using BepInEx.IL2CPP; using HarmonyLib; using Reactor.Networking; +using Reactor.Networking.MethodRpc; +using Reactor.Networking.Serialization; using Reactor.Patches; using Reactor.Unstrip; using UnhollowerBaseLib.Attributes; @@ -28,6 +30,8 @@ namespace Reactor PluginSingleton.Initialize(); RegisterInIl2CppAttribute.Initialize(); RegisterCustomRpcAttribute.Initialize(); + MessageConverterAttribute.Initialize(); + MethodRpcAttribute.Initialize(); ChainloaderHooks.OnPluginLoad(this); }