diff --git a/.gitignore b/.gitignore index f04d6913..dd9fb015 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ riderModule.iml /_ReSharper.Caches/ .idea /tools -.vscode +.vs* *.DotSettings.user \ No newline at end of file diff --git a/Reactor.Example/CustomRpcs.cs b/Reactor.Example/CustomRpcs.cs index 8adea47a..64aea25b 100644 --- a/Reactor.Example/CustomRpcs.cs +++ b/Reactor.Example/CustomRpcs.cs @@ -4,4 +4,6 @@ public enum CustomRpcCalls : uint { Example, MethodRpcExample, + TargetRpcExample, + HostRpcExample, } diff --git a/Reactor.Example/ExamplePlugin.cs b/Reactor.Example/ExamplePlugin.cs index c14e8b9c..fabb4687 100644 --- a/Reactor.Example/ExamplePlugin.cs +++ b/Reactor.Example/ExamplePlugin.cs @@ -99,4 +99,16 @@ public static void RpcSay(PlayerControl player, string text, float number, Playe { Logger.Info($"{player.Data.PlayerName} text: {text} number: {number} testPlayer: {testPlayer.NetId}"); } + + [MethodRpc((uint) CustomRpcCalls.TargetRpcExample, nameof(target))] + public static void RpcSayTo(PlayerControl player, PlayerControl target, string text) + { + Logger.Info($"{player.Data.PlayerName} text: {text} sentOnlyTo: {target.NetId}"); + } + + [MethodRpc((uint) CustomRpcCalls.HostRpcExample, true)] + public static void RpcSayToHost(PlayerControl player, string text) + { + Logger.Info($"{player.Data.PlayerName} text: {text} amHost: {AmongUsClient.Instance.AmHost} (should always be true)"); + } } diff --git a/Reactor/Networking/Attributes/MethodRpcAttribute.cs b/Reactor/Networking/Attributes/MethodRpcAttribute.cs index b0fa1b09..e414b205 100644 --- a/Reactor/Networking/Attributes/MethodRpcAttribute.cs +++ b/Reactor/Networking/Attributes/MethodRpcAttribute.cs @@ -23,6 +23,16 @@ public sealed class MethodRpcAttribute : Attribute /// public uint Id { get; } + /// + /// Gets the method's parameter to get the target client, if any. + /// + public string? TargetParameter { get; } + + /// + /// Gets a value indicating whether the RPC should only be sent to the host. + /// + public bool HostOnly { get; } + /// /// Gets or sets the send option of the rpc. /// @@ -39,6 +49,30 @@ public sealed class MethodRpcAttribute : Attribute [Obsolete("Non-immediate RPCs were removed in 2025.5.20. All RPCs are immediate. This property will be removed in a future version.")] public bool SendImmediately { get; set; } = true; + /// + /// Initializes a new instance of the class. + /// + /// The id of the rpc. + /// The parameter to get the target client from, if any. + public MethodRpcAttribute(uint id, string? targetParam) + { + Id = id; + TargetParameter = targetParam; + HostOnly = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// The id of the rpc. + /// Whether to send the RPC only to the host. + public MethodRpcAttribute(uint id, bool hostOnly) + { + Id = id; + TargetParameter = null; + HostOnly = hostOnly; + } + /// /// Initializes a new instance of the class. /// @@ -46,6 +80,8 @@ public sealed class MethodRpcAttribute : Attribute public MethodRpcAttribute(uint id) { Id = id; + TargetParameter = null; + HostOnly = false; } /// @@ -72,7 +108,7 @@ public static void Register(Assembly assembly, BasePlugin plugin) try { - var customRpc = new MethodRpc(plugin, method, attribute.Id, attribute.SendOption, attribute.LocalHandling); + var customRpc = new MethodRpc(plugin, method, attribute.Id, attribute.SendOption, attribute.LocalHandling, attribute.TargetParameter, attribute.HostOnly); PluginSingleton.Instance.CustomRpcManager.Register(customRpc); } catch (Exception e) diff --git a/Reactor/Networking/Rpc/MethodRpc.cs b/Reactor/Networking/Rpc/MethodRpc.cs index 49016355..84e1f836 100644 --- a/Reactor/Networking/Rpc/MethodRpc.cs +++ b/Reactor/Networking/Rpc/MethodRpc.cs @@ -32,11 +32,15 @@ public class MethodRpc : UnsafeCustomRpc /// The id of the rpc. /// The send option of the rpc. /// The local handling method of the rpc. - public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption option, RpcLocalHandling localHandling) : base(plugin, id) + /// The parameter to get the target client from, if any. + /// Whether to send the RPC only to the host. + public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption option, RpcLocalHandling localHandling, string? targetParam = null, bool hostOnly = false) : base(plugin, id) { Method = method; LocalHandling = localHandling; SendOption = option; + TargetParameter = targetParam; + HostOnly = hostOnly; var parameters = method.GetParameters(); @@ -66,6 +70,25 @@ public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption optio InnerNetObjectType = method.DeclaringType; } + if (TargetParameter != null) + { + if (HostOnly) + { + throw new ArgumentException("Cannot define rpc both addressed to host and define a Target Client."); + } + + var param = Array.Find(parameters, p => p.Name == TargetParameter); + if (param == null) + { + throw new ArgumentException("Target Client parameter isn't part of method rpc signature.", nameof(method)); + } + if (!typeof(InnerNetObject).IsAssignableFrom(param.ParameterType) && + !typeof(int).IsAssignableFrom(param.ParameterType)) + { + throw new ArgumentException("Target Client parameter has to be an InnerNetObject or int", nameof(method)); + } + } + _handle = Hook(method, parameters, method.IsStatic); } @@ -78,9 +101,11 @@ public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption optio /// The send option of the rpc. /// The local handling method of the rpc. /// The value indicating whether the rpc should be sent immediately. + /// The parameter to get the target client from, if any. + /// Whether to send the RPC only to the host. [Obsolete("Non-immediate RPCs were removed in 2025.5.20. All RPCs are immediate. Remove sendImmediately from the parameter list.")] - public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption option, RpcLocalHandling localHandling, bool sendImmediately) - : this(plugin, method, id, option, localHandling) + public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption option, RpcLocalHandling localHandling, bool sendImmediately, string? targetParam = null, bool hostOnly = false) + : this(plugin, method, id, option, localHandling, targetParam, hostOnly) { SendImmediately = sendImmediately; } @@ -90,6 +115,16 @@ public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption optio /// public MethodInfo Method { get; } + /// + /// Gets the method's parameter to get the target client, if any. + /// + public string? TargetParameter { get; } + + /// + /// Gets a value indicating whether the RPC is only sent to the host. + /// + public bool HostOnly { get; } + /// protected internal override bool IsSingleton => false; @@ -147,9 +182,10 @@ public override void UnsafeHandle(InnerNetObject innerNetObject, object? data) /// /// The to send the rpc on. /// The arguments to serialize and send. - public void Send(InnerNetObject innerNetObject, object[] args) + /// The id of the target client. -1 broadcasts without specific target. + public void Send(InnerNetObject innerNetObject, object[] args, int targetClientId = -1) { - UnsafeSend(innerNetObject, args); + UnsafeSend(innerNetObject, args, targetClientId: targetClientId); } private static readonly MethodInfo _sendMethod = AccessTools.Method(typeof(MethodRpc), nameof(Send)); @@ -176,6 +212,12 @@ DynamicMethod GenerateSender() dynamicMethod.DefineParameter(parameter.Position + (isStatic ? 0 : 1), ParameterAttributes.None, parameter.Name); } + var targetClientIndex = -1; + if (!HostOnly && TargetParameter != null) + { + targetClientIndex = Array.FindIndex(parameters, p => p.Name == TargetParameter); + } + var il = dynamicMethod.GetILGenerator(); // :yeefuckinhaw: @@ -231,6 +273,26 @@ DynamicMethod GenerateSender() il.Emit(OpCodes.Stelem_Ref); } + if (HostOnly) + { + il.Emit(OpCodes.Call, typeof(AmongUsClient).GetProperty(nameof(AmongUsClient.Instance), BindingFlags.Static | BindingFlags.Public)!.GetMethod!); + il.Emit(OpCodes.Callvirt, typeof(InnerNetClient).GetProperty(nameof(InnerNetClient.HostId), BindingFlags.Instance | BindingFlags.Public)!.GetMethod!); + } + else if (targetClientIndex >= 0) + { + il.Emit(OpCodes.Ldarg, targetClientIndex + (isStatic ? 0 : 1)); + + var parameter = parameters[targetClientIndex]; + if (typeof(InnerNetObject).IsAssignableFrom(parameter.ParameterType)) + { + il.Emit(OpCodes.Callvirt, typeof(InnerNetObject).GetProperty(nameof(InnerNetObject.OwnerId), BindingFlags.Instance | BindingFlags.Public)!.GetMethod!); + } + } + else + { + il.Emit(OpCodes.Ldc_I4_M1); + } + il.Emit(OpCodes.Call, _sendMethod); if (method.ReturnType != typeof(void))