Skip to content

Commit

Permalink
feat: syncvar hook with 1 arg (#1070)
Browse files Browse the repository at this point in the history
resolves: #1062
  • Loading branch information
James-Frowen committed Apr 14, 2022
1 parent c9c7bf7 commit 6e21877
Show file tree
Hide file tree
Showing 24 changed files with 693 additions and 59 deletions.
33 changes: 33 additions & 0 deletions Assets/Mirage/Runtime/CustomAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,39 @@ public class SyncVarAttribute : PropertyAttribute
/// If true this syncvar hook will also fire on the server side.
/// </summary>
public bool invokeHookOnServer;

/// <summary>
/// What type of look Mirage should look for
/// </summary>
public SyncHookType hookType = SyncHookType.Automatic;
}

public enum SyncHookType
{
/// <summary>
/// Looks for hooks matching the signature, gives compile error if none or more than 1 is found
/// </summary>
Automatic = 0,

/// <summary>
/// Hook with signature <c>void hookName(T newValue)</c>
/// </summary>
MethodWith1Arg,

/// <summary>
/// Hook with signature <c>void hookName(T oldValue, T newValue)</c>
/// </summary>
MethodWith2Arg,

/// <summary>
/// Hook with signature <c>event Action{T} hookName;</c>
/// </summary>
EventWith1Arg,

/// <summary>
/// Hook with signature <c>event Action{T,T} hookName;</c>
/// </summary>
EventWith2Arg,
}

/// <summary>
Expand Down
27 changes: 20 additions & 7 deletions Assets/Mirage/Weaver/Processors/SyncVarProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -348,17 +348,21 @@ void WriteCallHookMethodUsingField(ILProcessor worker, SyncVarHook hook, Variabl
void WriteCallHook(ILProcessor worker, SyncVarHook hook, VariableDefinition oldValue, FoundSyncVar syncVarField)
{
if (hook.Method != null)
WriteCallHookMethod(worker, hook.Method, oldValue, syncVarField);
WriteCallHookMethod(worker, hook.Method, hook.hookType, oldValue, syncVarField);
if (hook.Event != null)
WriteCallHookEvent(worker, hook.Event, oldValue, syncVarField);
WriteCallHookEvent(worker, hook.Event, hook.hookType, oldValue, syncVarField);
}

void WriteCallHookMethod(ILProcessor worker, MethodDefinition hookMethod, VariableDefinition oldValue, FoundSyncVar syncVarField)
void WriteCallHookMethod(ILProcessor worker, MethodDefinition hookMethod, SyncHookType hookType, VariableDefinition oldValue, FoundSyncVar syncVarField)
{
if (hookType != SyncHookType.MethodWith1Arg && hookType != SyncHookType.MethodWith2Arg)
throw new ArgumentException($"hook type should be method, but was {hookType}", nameof(hookType));

WriteStartFunctionCall();

// write args
WriteOldValue();
if (hookType == SyncHookType.MethodWith2Arg)
WriteOldValue();
WriteNewValue();

WriteEndFunctionCall();
Expand Down Expand Up @@ -415,13 +419,21 @@ void WriteEndFunctionCall()
}
}

void WriteCallHookEvent(ILProcessor worker, EventDefinition @event, VariableDefinition oldValue, FoundSyncVar syncVarField)
void WriteCallHookEvent(ILProcessor worker, EventDefinition @event, SyncHookType hookType, VariableDefinition oldValue, FoundSyncVar syncVarField)
{
if (hookType != SyncHookType.EventWith1Arg && hookType != SyncHookType.EventWith2Arg)
throw new ArgumentException($"hook type should be event, but was {hookType}", nameof(hookType));

// get backing field for event, and sure it is generic instance (eg MyType<T>.myEvent
FieldReference eventField = @event.DeclaringType.GetField(@event.Name).MakeHostGenericIfNeeded();

// get action type with number of args
Type actionType = hookType == SyncHookType.EventWith1Arg
? typeof(Action<>)
: typeof(Action<,>);

// get Invoke method and make it correct type
MethodReference invokeNonGeneric = @event.Module.ImportReference(typeof(Action<,>).GetMethod("Invoke"));
MethodReference invokeNonGeneric = module.ImportReference(actionType.GetMethod("Invoke"));
MethodReference invoke = invokeNonGeneric.MakeHostInstanceGeneric((GenericInstanceType)@event.EventType);

Instruction nopEvent = worker.Create(OpCodes.Nop);
Expand All @@ -442,7 +454,8 @@ void WriteCallHookEvent(ILProcessor worker, EventDefinition @event, VariableDefi
// **call invoke**
worker.Append(nopEvent);

WriteOldValue();
if (hookType == SyncHookType.EventWith2Arg)
WriteOldValue();
WriteNewValue();

worker.Append(worker.Create(OpCodes.Call, invoke));
Expand Down
163 changes: 130 additions & 33 deletions Assets/Mirage/Weaver/Processors/SyncVars/HookMethodFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,21 @@ namespace Mirage.Weaver.SyncVars
{
class SyncVarHook
{
public MethodDefinition Method;
public EventDefinition Event;
public readonly MethodDefinition Method;
public readonly EventDefinition Event;
public readonly SyncHookType hookType;

public SyncVarHook(MethodDefinition method, SyncHookType hookType)
{
Method = method;
this.hookType = hookType;
}
public SyncVarHook(EventDefinition @event, SyncHookType hookType)
{
Event = @event;
this.hookType = hookType;
}

}
internal static class HookMethodFinder
{
Expand All @@ -26,43 +39,110 @@ public static SyncVarHook GetHookMethod(FieldDefinition syncVar, TypeReference o
if (string.IsNullOrEmpty(hookFunctionName))
return null;

return FindHookMethod(syncVar, hookFunctionName, originalType);
SyncHookType hookType = syncVarAttr.GetField<SyncHookType>(nameof(SyncVarAttribute.hookType), SyncHookType.Automatic);

SyncVarHook hook = FindHookMethod(syncVar, hookFunctionName, hookType, originalType);
if (hook != null)
return hook;
else
throw new HookMethodException($"Could not find hook for '{syncVar.Name}', hook name '{hookFunctionName}', hook type {hookType}. See SyncHookType for valid signatures", syncVar);
}

static SyncVarHook FindHookMethod(FieldDefinition syncVar, string hookFunctionName, TypeReference originalType)
static SyncVarHook FindHookMethod(FieldDefinition syncVar, string hookFunctionName, SyncHookType hookType, TypeReference originalType)
{
// check event first
EventDefinition @event = syncVar.DeclaringType.Events.FirstOrDefault(x => x.Name == hookFunctionName);
if (@event != null)
switch (hookType)
{
return ValidateEvent(syncVar, originalType, @event);
default:
case SyncHookType.Automatic:
return FindAutomatic(syncVar, hookFunctionName, originalType);
case SyncHookType.MethodWith1Arg:
return FindMethod1Arg(syncVar, hookFunctionName, originalType);
case SyncHookType.MethodWith2Arg:
return FindMethod2Arg(syncVar, hookFunctionName, originalType);
case SyncHookType.EventWith1Arg:
return FindEvent1Arg(syncVar, hookFunctionName, originalType);
case SyncHookType.EventWith2Arg:
return FindEvent2Arg(syncVar, hookFunctionName, originalType);
}
}

MethodDefinition[] methods = syncVar.DeclaringType.GetMethods(hookFunctionName);
MethodDefinition[] methodsWith2Param = methods.Where(m => m.Parameters.Count == 2).ToArray();
private static SyncVarHook FindAutomatic(FieldDefinition syncVar, string hookFunctionName, TypeReference originalType)
{
SyncVarHook foundHook = null;

CheckHook(syncVar, hookFunctionName, ref foundHook, FindMethod1Arg(syncVar, hookFunctionName, originalType));
CheckHook(syncVar, hookFunctionName, ref foundHook, FindMethod2Arg(syncVar, hookFunctionName, originalType));
CheckHook(syncVar, hookFunctionName, ref foundHook, FindEvent1Arg(syncVar, hookFunctionName, originalType));
CheckHook(syncVar, hookFunctionName, ref foundHook, FindEvent2Arg(syncVar, hookFunctionName, originalType));

return foundHook;
}

static void CheckHook(FieldDefinition syncVar, string hookFunctionName, ref SyncVarHook foundHook, SyncVarHook newfound)
{
// dont need to check anything if new one is null (not found)
if (newfound == null)
return;

if (foundHook == null)
{
foundHook = newfound;
}
else
{
throw new HookMethodException($"Mutliple hooks found for '{syncVar.Name}', hook name '{hookFunctionName}'. Please set HookType or remove one of the overloads", syncVar);
}
}

private static SyncVarHook FindMethod1Arg(FieldDefinition syncVar, string hookFunctionName, TypeReference originalType)
{
return ValidateMethod(syncVar, hookFunctionName, originalType, 1);
}

private static SyncVarHook FindMethod2Arg(FieldDefinition syncVar, string hookFunctionName, TypeReference originalType)
{
return ValidateMethod(syncVar, hookFunctionName, originalType, 2);
}

if (methodsWith2Param.Length == 0)
private static SyncVarHook ValidateMethod(FieldDefinition syncVar, string hookFunctionName, TypeReference originalType, int argCount)
{
MethodDefinition[] methods = syncVar.DeclaringType.GetMethods(hookFunctionName);
MethodDefinition[] methodsWithParams = methods.Where(m => m.Parameters.Count == argCount).ToArray();
if (methodsWithParams.Length == 0)
{
throw new HookMethodException($"Could not find hook for '{syncVar.Name}', hook name '{hookFunctionName}'. " +
$"Method signature should be {HookParameterMessage(hookFunctionName, originalType)}",
syncVar);
return null;
}

foreach (MethodDefinition method in methodsWith2Param)
// return method if matching args are found
foreach (MethodDefinition method in methodsWithParams)
{
if (MatchesParameters(method, originalType))
if (MatchesParameters(method, originalType, argCount))
{
return new SyncVarHook { Method = method };
return new SyncVarHook(method, argCount == 1 ? SyncHookType.MethodWith1Arg : SyncHookType.MethodWith2Arg);
}
}

throw new HookMethodException($"Wrong type for Parameter in hook for '{syncVar.Name}', hook name '{hookFunctionName}'. " +
$"Method signature should be {HookParameterMessage(hookFunctionName, originalType)}",
syncVar);
// else throw saying args were wrong
throw new HookMethodException($"Wrong type for Parameter in hook for '{syncVar.Name}', hook name '{hookFunctionName}'.", syncVar);
}

private static SyncVarHook FindEvent1Arg(FieldDefinition syncVar, string hookFunctionName, TypeReference originalType)
{
return ValidateEvent(syncVar, originalType, hookFunctionName, 1);
}

private static SyncVarHook FindEvent2Arg(FieldDefinition syncVar, string hookFunctionName, TypeReference originalType)
{
return ValidateEvent(syncVar, originalType, hookFunctionName, 2);
}

private static SyncVarHook ValidateEvent(FieldDefinition syncVar, TypeReference originalType, EventDefinition @event)
private static SyncVarHook ValidateEvent(FieldDefinition syncVar, TypeReference originalType, string hookFunctionName, int argCount)
{
// we can't have 2 events/fields with same name, so using `First` is ok here
EventDefinition @event = syncVar.DeclaringType.Events.FirstOrDefault(x => x.Name == hookFunctionName);
if (@event == null)
return null;

TypeReference eventType = @event.EventType;
if (!eventType.FullName.Contains("System.Action"))
{
Expand All @@ -76,34 +156,51 @@ private static SyncVarHook ValidateEvent(FieldDefinition syncVar, TypeReference

var genericEvent = (GenericInstanceType)eventType;
Collection<TypeReference> args = genericEvent.GenericArguments;
if (args.Count != 2)
if (args.Count != argCount)
{
ThrowWrongHookType(syncVar, @event, eventType);
// ok to not have matching count
// we could be hookType.Automatic and looking for 1 arg, when there is event with 2 args
return null;
}

if (args[0].FullName != originalType.FullName || args[1].FullName != originalType.FullName)
if (MatchesParameters(genericEvent, originalType, argCount))
{
return new SyncVarHook(@event, argCount == 1 ? SyncHookType.EventWith1Arg : SyncHookType.EventWith2Arg);
}
else
{
ThrowWrongHookType(syncVar, @event, eventType);
}

return new SyncVarHook { Event = @event };
throw new InvalidOperationException("Code should never reach even, should return or throw ealier");
}

private static void ThrowWrongHookType(FieldDefinition syncVar, EventDefinition @event, TypeReference eventType)
{
throw new HookMethodException($"Hook Event for '{syncVar.Name}' needs to be type 'System.Action<,>' but was '{eventType.FullName}' instead", @event);
}

static string HookParameterMessage(string hookName, TypeReference ValueType)
=> string.Format("void {0}({1} oldValue, {1} newValue)", hookName, ValueType);


static bool MatchesParameters(MethodDefinition method, TypeReference originalType)
static bool MatchesParameters(GenericInstanceType genericEvent, TypeReference originalType, int count)
{
// matches event Action<T, T> eventName;
Collection<TypeReference> args = genericEvent.GenericArguments;
for (int i = 0; i < count; i++)
{
if (args[i].FullName != originalType.FullName)
return false;
}
return true;
}
static bool MatchesParameters(MethodDefinition method, TypeReference originalType, int count)
{
// matches void onValueChange(T oldValue, T newValue)
return method.Parameters[0].ParameterType.FullName == originalType.FullName &&
method.Parameters[1].ParameterType.FullName == originalType.FullName;
Collection<ParameterDefinition> parameters = method.Parameters;
for (int i = 0; i < count; i++)
{
if (parameters[i].ParameterType.FullName != originalType.FullName)
return false;
}
return true;
}

}
}

0 comments on commit 6e21877

Please sign in to comment.