Skip to content

Commit

Permalink
feat: adding attribute that allows for combination of checks
Browse files Browse the repository at this point in the history
Allows people to do a combination of condition with OR checks.

This is not possible with current attributes because they will be combine using AND, [Server,  HasAuthority] would require both server and hasAuthority to be true for the method to go away. Where as new [NetworkMethod(NetworkFlags.Server | NetworkFlags.HasAuthority)] would allow method to run if either server or HasAuthority to be true.
  • Loading branch information
James-Frowen committed Jul 21, 2023
1 parent 4690670 commit 18852f6
Show file tree
Hide file tree
Showing 5 changed files with 358 additions and 7 deletions.
41 changes: 41 additions & 0 deletions Assets/Mirage/Runtime/CustomAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,47 @@ public class LocalPlayerAttribute : Attribute
public bool error = true;
}

/// <summary>
/// Prevents this method from running unless the NetworkFlags match the current state
/// <para>Can only be used inside a NetworkBehaviour</para>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class NetworkMethodAttribute : Attribute
{
/// <summary>
/// If true, if called incorrectly method will throw.<br/>
/// If false, no error is thrown, but the method won't execute.<br/>
/// <para>
/// useful for unity built in methods such as Await, Update, Start, etc.
/// </para>
/// </summary>
public bool error = true;

public NetworkMethodAttribute(NetworkFlags flags) { }
}

[Flags]
public enum NetworkFlags
{
// note: NotActive can't be 0 as it needs its own flag
// This is so that people can check for (Server | NotActive)
/// <summary>
/// If both server and client are not active. Can be used to check for singleplayer or unspawned object
/// </summary>
NotActive = 1,
Server = 2,
Client = 4,
/// <summary>
/// If either Server or Client is active.
/// <para>
/// Note this will not check host mode. For host mode you need to use <see cref="ServerAttribute"/> and <see cref="ClientAttribute"/>
/// </para>
/// </summary>
Active = Server | Client,
HasAuthority = 8,
LocalOwner = 16,
}

/// <summary>
/// Converts a string property into a Scene property in the inspector
/// </summary>
Expand Down
89 changes: 83 additions & 6 deletions Assets/Mirage/Weaver/Processors/AttributeProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ private void ProcessMethodAttributes(MethodDefinition md, FoundType foundType)
InjectGuard<ClientAttribute>(md, foundType, IsClient, "[Client] function '{0}' called when client not active");
InjectGuard<HasAuthorityAttribute>(md, foundType, HasAuthority, "[Has Authority] function '{0}' called on player without authority");
InjectGuard<LocalPlayerAttribute>(md, foundType, IsLocalPlayer, "[Local Player] function '{0}' called on nonlocal player");
InjectNetworkMethodGuard(md, foundType);
CheckAttribute<ServerRpcAttribute>(md, foundType);
CheckAttribute<ClientRpcAttribute>(md, foundType);
}
Expand All @@ -115,32 +116,39 @@ private void CheckAttribute<TAttribute>(MethodDefinition md, FoundType foundType
}
}

private void InjectGuard<TAttribute>(MethodDefinition md, FoundType foundType, MethodReference predicate, string format)
private bool TryGetAttribte<TAttribute>(MethodDefinition md, FoundType foundType, out CustomAttribute attribute)
{
var attribute = md.GetCustomAttribute<TAttribute>();
attribute = md.GetCustomAttribute<TAttribute>();
if (attribute == null)
return;
return false;

if (md.IsAbstract)
{
logger.Error($"{typeof(TAttribute)} can't be applied to abstract method. Apply to override methods instead.", md);
return;
return false;
}

if (!foundType.IsNetworkBehaviour)
{
logger.Error($"{attribute.AttributeType.Name} method {md.Name} must be declared in a NetworkBehaviour", md);
return;
return false;
}

if (md.Name == "Awake" && !md.HasParameters)
{
logger.Error($"{attribute.AttributeType.Name} will not work on the Awake method.", md);
return;
return false;
}

// dont need to set modified for errors, so we set it here when we start doing ILProcessing
modified = true;
return true;
}

private void InjectGuard<TAttribute>(MethodDefinition md, FoundType foundType, MethodReference predicate, string format)
{
if (!TryGetAttribte<TAttribute>(md, foundType, out var attribute))
return;

var throwError = attribute.GetField("error", true);
var worker = md.Body.GetILProcessor();
Expand All @@ -149,6 +157,7 @@ private void InjectGuard<TAttribute>(MethodDefinition md, FoundType foundType, M
worker.InsertBefore(top, worker.Create(OpCodes.Ldarg_0));
worker.InsertBefore(top, worker.Create(OpCodes.Call, predicate));
worker.InsertBefore(top, worker.Create(OpCodes.Brtrue, top));

if (throwError)
{
var message = string.Format(format, md.Name);
Expand All @@ -165,6 +174,74 @@ private void InjectGuard<TAttribute>(MethodDefinition md, FoundType foundType, M
}
}

private void InjectNetworkMethodGuard(MethodDefinition md, FoundType foundType)
{
if (!TryGetAttribte<NetworkMethodAttribute>(md, foundType, out var attribute))
return;

// Get the required flags from the attribute constructor argument
var requiredFlagsValue = (NetworkFlags)attribute.ConstructorArguments[0].Value;
var throwError = attribute.GetField("error", true);
var worker = md.Body.GetILProcessor();
var top = md.Body.Instructions[0];

// check for each flag
// if true, then jump to start of code
// this should act as an OR check
if (requiredFlagsValue.HasFlag(NetworkFlags.Server))
{
worker.InsertBefore(top, worker.Create(OpCodes.Ldarg_0));
worker.InsertBefore(top, worker.Create(OpCodes.Call, IsServer));
worker.InsertBefore(top, worker.Create(OpCodes.Brtrue, top));
}
if (requiredFlagsValue.HasFlag(NetworkFlags.Client))
{
worker.InsertBefore(top, worker.Create(OpCodes.Ldarg_0));
worker.InsertBefore(top, worker.Create(OpCodes.Call, IsClient));
worker.InsertBefore(top, worker.Create(OpCodes.Brtrue, top));
}
if (requiredFlagsValue.HasFlag(NetworkFlags.HasAuthority))
{
worker.InsertBefore(top, worker.Create(OpCodes.Ldarg_0));
worker.InsertBefore(top, worker.Create(OpCodes.Call, HasAuthority));
worker.InsertBefore(top, worker.Create(OpCodes.Brtrue, top));
}
if (requiredFlagsValue.HasFlag(NetworkFlags.LocalOwner))
{
// Check if the object is the local player's
worker.InsertBefore(top, worker.Create(OpCodes.Ldarg_0));
worker.InsertBefore(top, worker.Create(OpCodes.Call, IsLocalPlayer));
worker.InsertBefore(top, worker.Create(OpCodes.Brtrue, top));
}

if (requiredFlagsValue.HasFlag(NetworkFlags.NotActive))
{
// Check if neither Server nor Clients are active
worker.InsertBefore(top, worker.Create(OpCodes.Ldarg_0));
worker.InsertBefore(top, worker.Create(OpCodes.Call, IsServer));
worker.InsertBefore(top, worker.Create(OpCodes.Ldarg_0));
worker.InsertBefore(top, worker.Create(OpCodes.Call, IsClient));
worker.InsertBefore(top, worker.Create(OpCodes.Or));
worker.InsertBefore(top, worker.Create(OpCodes.Brfalse, top));
}

if (throwError)
{
var message = $"Method '{md.Name}' cannot be executed as {nameof(NetworkFlags)} condition is not met.";
worker.InsertBefore(top, worker.Create(OpCodes.Ldstr, message));
worker.InsertBefore(top, worker.Create(OpCodes.Newobj, () => new MethodInvocationException("")));
worker.InsertBefore(top, worker.Create(OpCodes.Throw));
}
else
{
// dont need to set param or return if we throw
InjectGuardParameters(md, worker, top);
InjectGuardReturnValue(md, worker, top);
worker.InsertBefore(top, worker.Create(OpCodes.Ret));
}
}


// this is required to early-out from a function with "out" parameters
private static void InjectGuardParameters(MethodDefinition md, ILProcessor worker, Instruction top)
{
Expand Down
Loading

0 comments on commit 18852f6

Please sign in to comment.