Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ riderModule.iml
/_ReSharper.Caches/
.idea
/tools
.vscode
.vs*
*.DotSettings.user
2 changes: 2 additions & 0 deletions Reactor.Example/CustomRpcs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ public enum CustomRpcCalls : uint
{
Example,
MethodRpcExample,
TargetRpcExample,
HostRpcExample,
}
12 changes: 12 additions & 0 deletions Reactor.Example/ExamplePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,16 @@ public static void RpcSay(PlayerControl player, string text, float number, Playe
{
Logger<ExamplePlugin>.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<ExamplePlugin>.Info($"{player.Data.PlayerName} text: {text} sentOnlyTo: {target.NetId}");
}

[MethodRpc((uint) CustomRpcCalls.HostRpcExample, true)]
public static void RpcSayToHost(PlayerControl player, string text)
{
Logger<ExamplePlugin>.Info($"{player.Data.PlayerName} text: {text} amHost: {AmongUsClient.Instance.AmHost} (should always be true)");
}
}
38 changes: 37 additions & 1 deletion Reactor/Networking/Attributes/MethodRpcAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ public sealed class MethodRpcAttribute : Attribute
/// </summary>
public uint Id { get; }

/// <summary>
/// Gets the method's parameter to get the target client, if any.
/// </summary>
public string? TargetParameter { get; }

/// <summary>
/// Gets a value indicating whether the RPC should only be sent to the host.
/// </summary>
public bool HostOnly { get; }

/// <summary>
/// Gets or sets the send option of the rpc.
/// </summary>
Expand All @@ -39,13 +49,39 @@ 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;

/// <summary>
/// Initializes a new instance of the <see cref="MethodRpcAttribute"/> class.
/// </summary>
/// <param name="id">The id of the rpc.</param>
/// <param name="targetParam">The parameter to get the target client from, if any.</param>
public MethodRpcAttribute(uint id, string? targetParam)
{
Id = id;
TargetParameter = targetParam;
HostOnly = false;
}

/// <summary>
/// Initializes a new instance of the <see cref="MethodRpcAttribute"/> class.
/// </summary>
/// <param name="id">The id of the rpc.</param>
/// <param name="hostOnly">Whether to send the RPC only to the host.</param>
public MethodRpcAttribute(uint id, bool hostOnly)
{
Id = id;
TargetParameter = null;
HostOnly = hostOnly;
}

/// <summary>
/// Initializes a new instance of the <see cref="MethodRpcAttribute"/> class.
/// </summary>
/// <param name="id">The id of the rpc.</param>
public MethodRpcAttribute(uint id)
Comment thread
TheDavSmasher marked this conversation as resolved.
{
Id = id;
TargetParameter = null;
HostOnly = false;
}

/// <summary>
Expand All @@ -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<ReactorPlugin>.Instance.CustomRpcManager.Register(customRpc);
}
catch (Exception e)
Expand Down
72 changes: 67 additions & 5 deletions Reactor/Networking/Rpc/MethodRpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ public class MethodRpc : UnsafeCustomRpc
/// <param name="id">The id of the rpc.</param>
/// <param name="option">The send option of the rpc.</param>
/// <param name="localHandling">The local handling method of the rpc.</param>
public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption option, RpcLocalHandling localHandling) : base(plugin, id)
/// <param name="targetParam">The parameter to get the target client from, if any.</param>
/// <param name="hostOnly">Whether to send the RPC only to the host.</param>
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();

Expand Down Expand Up @@ -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);
}

Expand All @@ -78,9 +101,11 @@ public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption optio
/// <param name="option">The send option of the rpc.</param>
/// <param name="localHandling">The local handling method of the rpc.</param>
/// <param name="sendImmediately">The value indicating whether the rpc should be sent immediately.</param>
/// <param name="targetParam">The parameter to get the target client from, if any.</param>
/// <param name="hostOnly">Whether to send the RPC only to the host.</param>
[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;
}
Expand All @@ -90,6 +115,16 @@ public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption optio
/// </summary>
public MethodInfo Method { get; }

/// <summary>
/// Gets the method's parameter to get the target client, if any.
/// </summary>
public string? TargetParameter { get; }

/// <summary>
/// Gets a value indicating whether the RPC is only sent to the host.
/// </summary>
public bool HostOnly { get; }

/// <inheritdoc />
protected internal override bool IsSingleton => false;

Expand Down Expand Up @@ -147,9 +182,10 @@ public override void UnsafeHandle(InnerNetObject innerNetObject, object? data)
/// </summary>
/// <param name="innerNetObject">The <see cref="InnerNetObject"/> to send the rpc on.</param>
/// <param name="args">The arguments to serialize and send.</param>
public void Send(InnerNetObject innerNetObject, object[] args)
/// <param name="targetClientId">The id of the target client. -1 broadcasts without specific target.</param>
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));
Expand All @@ -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:
Expand Down Expand Up @@ -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))
Expand Down