-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(BehaviourStateRequirement): allow returning early in Behaviours
In a `UnityEngine.Behaviour` or a type derived from it it is often useful to return early while the Behaviour or the GameObject the Behaviour is on are disabled/inactive. This new weaver takes care of adding the necessary instructions to a method to return early based on those two states.
- Loading branch information
Christopher-Marcel Böddecker
committed
Jan 21, 2019
1 parent
d11cc84
commit ee9955a
Showing
9 changed files
with
331 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
Sources/BehaviourStateRequirementMethod.Fody/BehaviourStateRequirementMethod.Fody.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>netstandard2.0</TargetFrameworks> | ||
<AssemblyName>Malimbe.BehaviourStateRequirementMethod.Fody</AssemblyName> | ||
<RootNamespace>Malimbe.BehaviourStateRequirementMethod.Fody</RootNamespace> | ||
<LangVersion>latest</LangVersion> | ||
<AttributeProjectName>$([MSBuild]::ValueOrDefault('$(MSBuildProjectName)', '').Replace('.Fody', ''))</AttributeProjectName> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="FodyHelpers" Version="3.3.5" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\$(AttributeProjectName)\$(AttributeProjectName).csproj" /> | ||
<ProjectReference Include="..\Shared\Shared.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
211 changes: 211 additions & 0 deletions
211
Sources/BehaviourStateRequirementMethod.Fody/ModuleWeaver.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
namespace Malimbe.BehaviourStateRequirementMethod.Fody | ||
{ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using global::Fody; | ||
using Malimbe.Shared; | ||
using Mono.Cecil; | ||
using Mono.Cecil.Cil; | ||
using Mono.Cecil.Rocks; | ||
using Mono.Collections.Generic; | ||
|
||
// ReSharper disable once UnusedMember.Global | ||
public sealed class ModuleWeaver : BaseModuleWeaver | ||
{ | ||
private static readonly string _fullAttributeName = typeof(RequiresBehaviourStateAttribute).FullName; | ||
|
||
private TypeReference _behaviourTypeReference; | ||
private MethodReference _getGameObjectMethodReference; | ||
private MethodReference _getActiveSelfMethodReference; | ||
private MethodReference _getActiveInHierarchyMethodReference; | ||
private MethodReference _getIsActiveAndEnabledMethodReference; | ||
private MethodReference _getEnabledMethodReference; | ||
|
||
public override bool ShouldCleanReference => | ||
true; | ||
|
||
public override void Execute() | ||
{ | ||
FindReferences(); | ||
|
||
IEnumerable<MethodDefinition> methodDefinitions = | ||
ModuleDefinition.Types.SelectMany(definition => definition.Methods); | ||
foreach (MethodDefinition methodDefinition in methodDefinitions) | ||
{ | ||
if (!FindAndRemoveAttribute(methodDefinition, out CustomAttribute attribute)) | ||
{ | ||
continue; | ||
} | ||
|
||
GameObjectActivity gameObjectActivity = (GameObjectActivity)attribute.ConstructorArguments[0].Value; | ||
bool behaviourNeedsToBeEnabled = (bool)attribute.ConstructorArguments[1].Value; | ||
|
||
if (gameObjectActivity == GameObjectActivity.None && !behaviourNeedsToBeEnabled) | ||
{ | ||
LogWarning( | ||
$"The method '{methodDefinition.FullName}' is annotated to require a Behaviour state" | ||
+ " but the attribute constructor arguments result in no action being taken."); | ||
continue; | ||
} | ||
|
||
MethodBody body = methodDefinition.Body; | ||
Collection<Instruction> instructions = body.Instructions; | ||
if (instructions.Count == 0) | ||
{ | ||
LogWarning( | ||
$"The method '{methodDefinition.FullName}' is annotated to require a Behaviour state" | ||
+ " but the method has no instructions in its body and thus no action is being taken."); | ||
continue; | ||
} | ||
|
||
body.SimplifyMacros(); | ||
InsertInstructions( | ||
body, | ||
methodDefinition, | ||
instructions, | ||
gameObjectActivity, | ||
behaviourNeedsToBeEnabled); | ||
body.OptimizeMacros(); | ||
} | ||
} | ||
|
||
public override IEnumerable<string> GetAssembliesForScanning() | ||
{ | ||
yield return "UnityEngine"; | ||
} | ||
|
||
private void FindReferences() | ||
{ | ||
MethodReference ImportPropertyGetter(TypeDefinition typeDefinition, string propertyName) => | ||
ModuleDefinition.ImportReference( | ||
typeDefinition.Properties.Single(definition => definition.Name == propertyName).GetMethod); | ||
|
||
TypeDefinition behaviourTypeDefinition = FindType("UnityEngine.Behaviour"); | ||
TypeDefinition gameObjectTypeDefinition = FindType("UnityEngine.GameObject"); | ||
|
||
_behaviourTypeReference = ModuleDefinition.ImportReference(behaviourTypeDefinition); | ||
_getGameObjectMethodReference = ImportPropertyGetter(FindType("UnityEngine.Component"), "gameObject"); | ||
_getActiveSelfMethodReference = ImportPropertyGetter(gameObjectTypeDefinition, "activeSelf"); | ||
_getActiveInHierarchyMethodReference = ImportPropertyGetter(gameObjectTypeDefinition, "activeInHierarchy"); | ||
_getIsActiveAndEnabledMethodReference = | ||
ImportPropertyGetter(behaviourTypeDefinition, "isActiveAndEnabled"); | ||
_getEnabledMethodReference = ImportPropertyGetter(behaviourTypeDefinition, "enabled"); | ||
} | ||
|
||
private bool FindAndRemoveAttribute(MethodDefinition methodDefinition, out CustomAttribute foundAttribute) | ||
{ | ||
foundAttribute = methodDefinition.CustomAttributes.SingleOrDefault( | ||
attribute => attribute.AttributeType.FullName == _fullAttributeName); | ||
if (foundAttribute == null) | ||
{ | ||
return false; | ||
} | ||
|
||
methodDefinition.CustomAttributes.Remove(foundAttribute); | ||
LogInfo($"Removed the attribute '{_fullAttributeName}' from the method '{methodDefinition.FullName}'."); | ||
|
||
if (methodDefinition.DeclaringType.IsSubclassOf(_behaviourTypeReference)) | ||
{ | ||
return true; | ||
} | ||
|
||
LogError( | ||
$"The method '{methodDefinition.FullName}' is annotated to require a Behaviour state" | ||
+ $" but the declaring type doesn't derive from '{_behaviourTypeReference.FullName}'."); | ||
return false; | ||
} | ||
|
||
private void InsertInstructions( | ||
MethodBody body, | ||
MethodReference methodDefinition, | ||
IList<Instruction> instructions, | ||
GameObjectActivity gameObjectActivity, | ||
bool behaviourNeedsToBeEnabled) | ||
{ | ||
Instruction earlyReturnInstruction; | ||
|
||
if (methodDefinition.ReturnType.FullName != TypeSystem.VoidReference.FullName) | ||
{ | ||
// Create new variable to return a value | ||
VariableDefinition variableDefinition = new VariableDefinition(methodDefinition.ReturnType); | ||
body.Variables.Add(variableDefinition); | ||
// Set variable to default value | ||
body.InitLocals = true; | ||
|
||
// Load variable | ||
Instruction loadInstruction = Instruction.Create(OpCodes.Ldloc, variableDefinition); | ||
instructions.Add(loadInstruction); | ||
// Return | ||
instructions.Add(Instruction.Create(OpCodes.Ret)); | ||
|
||
earlyReturnInstruction = loadInstruction; | ||
} | ||
else | ||
{ | ||
earlyReturnInstruction = instructions.Last(instruction => instruction.OpCode == OpCodes.Ret); | ||
} | ||
|
||
int index = -1; | ||
|
||
if (gameObjectActivity == GameObjectActivity.InHierarchy && behaviourNeedsToBeEnabled) | ||
{ | ||
// Load this (for isActiveAndEnabled getter call) | ||
instructions.Insert(++index, Instruction.Create(OpCodes.Ldarg_0)); | ||
// Call isActiveAndEnabled getter | ||
instructions.Insert( | ||
++index, | ||
Instruction.Create(OpCodes.Callvirt, _getIsActiveAndEnabledMethodReference)); | ||
|
||
AddEarlyReturnInstruction(instructions, ref index, earlyReturnInstruction); | ||
} | ||
else | ||
{ | ||
if (gameObjectActivity != GameObjectActivity.None) | ||
{ | ||
// Load this (for gameObject getter call) | ||
instructions.Insert(++index, Instruction.Create(OpCodes.Ldarg_0)); | ||
// Call gameObject getter | ||
instructions.Insert(++index, Instruction.Create(OpCodes.Callvirt, _getGameObjectMethodReference)); | ||
|
||
// ReSharper disable once SwitchStatementMissingSomeCases | ||
switch (gameObjectActivity) | ||
{ | ||
case GameObjectActivity.Self: | ||
// Call activeSelf getter | ||
instructions.Insert( | ||
++index, | ||
Instruction.Create(OpCodes.Callvirt, _getActiveSelfMethodReference)); | ||
break; | ||
case GameObjectActivity.InHierarchy: | ||
// Call activeInHierarchy getter | ||
instructions.Insert( | ||
++index, | ||
Instruction.Create(OpCodes.Callvirt, _getActiveInHierarchyMethodReference)); | ||
break; | ||
} | ||
|
||
AddEarlyReturnInstruction(instructions, ref index, earlyReturnInstruction); | ||
} | ||
|
||
if (behaviourNeedsToBeEnabled) | ||
{ | ||
// Load this (for enabled getter call) | ||
instructions.Insert(++index, Instruction.Create(OpCodes.Ldarg_0)); | ||
// Call enabled getter | ||
instructions.Insert(++index, Instruction.Create(OpCodes.Callvirt, _getEnabledMethodReference)); | ||
|
||
AddEarlyReturnInstruction(instructions, ref index, earlyReturnInstruction); | ||
} | ||
} | ||
|
||
LogInfo($"Added (an) early return(s) to the method '{methodDefinition.FullName}'."); | ||
} | ||
|
||
private static void AddEarlyReturnInstruction( | ||
IList<Instruction> instructions, | ||
ref int index, | ||
Instruction earlyReturnInstruction) => | ||
// Return early if false | ||
instructions.Insert(++index, Instruction.Create(OpCodes.Brfalse, earlyReturnInstruction)); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
Sources/BehaviourStateRequirementMethod/BehaviourStateRequirementMethod.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>netstandard2.0</TargetFrameworks> | ||
<AssemblyName>Malimbe.BehaviourStateRequirementMethod</AssemblyName> | ||
<RootNamespace>Malimbe.BehaviourStateRequirementMethod</RootNamespace> | ||
<LangVersion>latest</LangVersion> | ||
</PropertyGroup> | ||
|
||
</Project> |
21 changes: 21 additions & 0 deletions
21
Sources/BehaviourStateRequirementMethod/GameObjectActivity.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
namespace Malimbe.BehaviourStateRequirementMethod | ||
{ | ||
/// <summary> | ||
/// The active state of a GameObject. | ||
/// </summary> | ||
public enum GameObjectActivity | ||
{ | ||
/// <summary> | ||
/// The GameObject active state is of no interest. | ||
/// </summary> | ||
None = 0, | ||
/// <summary> | ||
/// The GameObject itself needs to be active, the state of parent GameObjects is ignored. | ||
/// </summary> | ||
Self, | ||
/// <summary> | ||
/// The GameObject is active in the scene because it is active itself and all parent GameObjects are, too. | ||
/// </summary> | ||
InHierarchy | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
Sources/BehaviourStateRequirementMethod/RequiresBehaviourStateAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
namespace Malimbe.BehaviourStateRequirementMethod | ||
{ | ||
using System; | ||
|
||
/// <summary> | ||
/// Indicates that the method returns early in case a specific GameObject state or Behaviour state isn't matched. | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Method)] | ||
public sealed class RequiresBehaviourStateAttribute : Attribute | ||
{ | ||
// ReSharper disable MemberCanBePrivate.Global | ||
/// <summary> | ||
/// The required active state of the GameObject that the component the method is on is added to. | ||
/// </summary> | ||
public readonly GameObjectActivity GameObjectActivity; | ||
/// <summary> | ||
/// The required state of the Behaviour. | ||
/// </summary> | ||
public readonly bool BehaviourNeedsToBeEnabled; | ||
// ReSharper restore MemberCanBePrivate.Global | ||
|
||
/// <summary> | ||
/// Indicates that the method returns early in case a specific GameObject state or Behaviour state isn't matched. | ||
/// </summary> | ||
/// <param name="gameObjectActivity">The required active state of the GameObject that the component the method is on is added to.</param> | ||
/// <param name="behaviourNeedsToBeEnabled">The required state of the Behaviour.</param> | ||
public RequiresBehaviourStateAttribute( | ||
GameObjectActivity gameObjectActivity = GameObjectActivity.InHierarchy, | ||
bool behaviourNeedsToBeEnabled = true) | ||
{ | ||
GameObjectActivity = gameObjectActivity; | ||
BehaviourNeedsToBeEnabled = behaviourNeedsToBeEnabled; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters