diff --git a/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/CodeGeneratorUtils.cs b/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/CodeGeneratorUtils.cs index fa2426786..76fb0b047 100644 --- a/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/CodeGeneratorUtils.cs +++ b/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/CodeGeneratorUtils.cs @@ -1,15 +1,24 @@ +using System.Linq; using System.Text.RegularExpressions; #if UNITY_EDITOR +using System; +using Codice.Client.Common.TreeGrouper; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using UnityEditor; +using UnityEngine; #endif namespace RegressionGames { #if UNITY_EDITOR - public class CodeGeneratorUtils + public static class CodeGeneratorUtils { - public static readonly string HeaderComment = $"/*\r\n* This file has been automatically generated. Do not modify.\r\n*/\r\n\r\n"; - + public static readonly string HeaderComment = "/// " + Environment.NewLine + + "/// This file has been automatically generated. Do not modify." + Environment.NewLine + + "/// " + Environment.NewLine + Environment.NewLine; + public static string SanitizeActionName(string name) { return Regex.Replace(name.Replace(" ", "_"), "[^0-9a-zA-Z_]", "_"); @@ -22,6 +31,33 @@ public static string GetNamespaceForProject() { return Regex.Replace("RG" + PlayerSettings.productName.Replace(" ", "_"), "[^0-9a-zA-Z_]", "_"); } + + public static NameSyntax QualifiedNameFor(Type type) => SyntaxFactory.ParseName(type.FullName); + + public static AttributeSyntax Attribute(Type attributeType, params ExpressionSyntax[] arguments) + { + return SyntaxFactory.Attribute( + QualifiedNameFor(attributeType), + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SeparatedList(arguments.Select(SyntaxFactory.AttributeArgument)))); + } + + public static TypeOfExpressionSyntax TypeOf(Type t) => SyntaxFactory.TypeOfExpression(QualifiedNameFor(t)); + public static TypeOfExpressionSyntax TypeOf(string fullTypeName) => SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseName(fullTypeName)); + public static ClassDeclarationSyntax PartialClass(string name) => SyntaxFactory.ClassDeclaration(name) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PartialKeyword))); + + public static MemberDeclarationSyntax CreatePartialAttacherClass( + string ns, string name, params string[] componentTypeNames) + { + var classDeclaration = PartialClass(name) + .AddAttributeLists(SyntaxFactory.AttributeList(SyntaxFactory.SeparatedList( + componentTypeNames.Select(componentType => + Attribute(typeof(RequireComponent), TypeOf(componentType)))))); + return string.IsNullOrEmpty(ns) + ? classDeclaration + : SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(ns)).AddMembers(classDeclaration); + } } #endif } diff --git a/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGActionClasses.cs b/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGActionClasses.cs index ae935c232..000df179a 100644 --- a/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGActionClasses.cs +++ b/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGActionClasses.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using RegressionGames.RGBotConfigs; using UnityEditor; #endif @@ -19,7 +20,7 @@ public static class GenerateRGActionClasses { public static void Generate(List actionInfos) { - Dictionary fileWriteTasks = new(); + Dictionary fileWriteTasks = new(); // Iterate through BotActions foreach (var botAction in actionInfos) { @@ -41,11 +42,12 @@ public static void Generate(List actionInfos) } var projectNamespace = CodeGeneratorUtils.GetNamespaceForProject(); - + botAction.GeneratedClassName = $"{projectNamespace}.RGAction_{CodeGeneratorUtils.SanitizeActionName(botAction.ActionName)}"; // Create a new compilation unit + var actionClassName = $"RGAction_{CodeGeneratorUtils.SanitizeActionName(botAction.ActionName)}"; CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit() .AddUsings( usings.Select(v => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(v))).ToArray() @@ -56,8 +58,13 @@ public static void Generate(List actionInfos) .AddMembers( // Class declaration SyntaxFactory - .ClassDeclaration( - $"RGAction_{CodeGeneratorUtils.SanitizeActionName(botAction.ActionName)}") + .ClassDeclaration(actionClassName) + .AddAttributeLists(SyntaxFactory.AttributeList(SyntaxFactory.SeparatedList(new[] + { + CodeGeneratorUtils.Attribute(typeof(DisallowMultipleComponent)), + CodeGeneratorUtils.Attribute(typeof(RequireComponent), + CodeGeneratorUtils.TypeOf(typeof(RGEntity))) + }))) .AddModifiers( SyntaxFactory.Token(SyntaxKind.PublicKeyword) // Only add one of the "class" keywords here @@ -87,10 +94,15 @@ public static void Generate(List actionInfos) GenerateActionRequestConstructor(botAction) ) ) - ); + ) + .AddMembers(CodeGeneratorUtils.CreatePartialAttacherClass( + botAction.Namespace, + botAction.Object, + typeof(RGEntity).FullName, + $"{projectNamespace}.{actionClassName}")); // Format the generated code - string formattedCode = compilationUnit.NormalizeWhitespace().ToFullString(); + string formattedCode = compilationUnit.NormalizeWhitespace(eol: Environment.NewLine).ToFullString(); // Save to 'Assets/RegressionGames/Runtime/GeneratedScripts/RGActions,RGSerialization.cs' string fileName = $"RGAction_{CodeGeneratorUtils.SanitizeActionName(botAction.ActionName)}.cs"; @@ -167,7 +179,7 @@ private static MemberDeclarationSyntax GenerateGetActionNameMethod(RGActionAttri return getActionNameMethod; } - + private static ArgumentSyntax GenerateActionDelegate(RGActionAttributeInfo action) { // Generate the GetComponent().MethodName piece for both cases (0 and non-0 parameters) @@ -236,14 +248,14 @@ private static MemberDeclarationSyntax GenerateStartActionMethod(RGActionInfo ac foreach (var parameter in action.Parameters) { string paramName = parameter.Name; - + methodInvocationArguments.Add(paramName); parameterParsingStatements.Add(SyntaxFactory.ParseStatement($"{parameter.Type} {paramName} = default;")); parameterParsingStatements.Add(SyntaxFactory.IfStatement(IfCondition(parameter), IfBody(parameter), ElseBody(parameter))); } - string methodInvocationArgumentsString = methodInvocationArguments.Count > 0 ? - ", " + string.Join(", ", methodInvocationArguments) : + string methodInvocationArgumentsString = methodInvocationArguments.Count > 0 ? + ", " + string.Join(", ", methodInvocationArguments) : string.Empty; parameterParsingStatements.Add(SyntaxFactory.ParseStatement($"Invoke(\"{action.ActionName}\"{methodInvocationArgumentsString});")); @@ -264,16 +276,16 @@ private static InvocationExpressionSyntax IfCondition(RGParameterInfo param) { return SyntaxFactory.InvocationExpression( SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName("input"), + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("input"), SyntaxFactory.IdentifierName("TryGetValue") )).WithArgumentList( SyntaxFactory.ArgumentList( - SyntaxFactory.SeparatedList(new List - { + SyntaxFactory.SeparatedList(new List + { SyntaxFactory.Argument( SyntaxFactory.LiteralExpression( - SyntaxKind.StringLiteralExpression, + SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(param.Name) ) ), @@ -291,13 +303,13 @@ private static InvocationExpressionSyntax IfCondition(RGParameterInfo param) * { * string: * key = keyInput.ToString(); - * + * * primitive: * KeyType.TryParse(keyInput.ToString(), out key); * * nonprimitive: * key = RGSerialization.Deserialize_KeyType(key.ToString()); - * + * * } * catch (Exception ex) * { @@ -309,7 +321,7 @@ private static StatementSyntax IfBody(RGParameterInfo param) { var paramType = param.Type; var paramName = param.Name; - + string tryParseStatement; if (paramType.ToLower() == "string" || paramType.ToLower() == "system.string") { @@ -321,7 +333,7 @@ private static StatementSyntax IfBody(RGParameterInfo param) } else { - // Do direct type cast whenever possible, taking into account nullable types + // Do direct type cast whenever possible, taking into account nullable types tryParseStatement = $"if ({paramName}Input is {paramType.Replace("?", "")}"; if (param.Nullable) { @@ -368,15 +380,15 @@ private static ElseClauseSyntax ElseBody(RGParameterInfo param) { return default(ElseClauseSyntax); } - + // Validation check for key existence if param must be non-null - return SyntaxFactory.ElseClause(SyntaxFactory.Block(new StatementSyntax[] + return SyntaxFactory.ElseClause(SyntaxFactory.Block(new StatementSyntax[] { SyntaxFactory.ExpressionStatement( SyntaxFactory.InvocationExpression( SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName("RGDebug"), + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("RGDebug"), SyntaxFactory.IdentifierName("LogError") ) ).WithArgumentList( @@ -384,14 +396,14 @@ private static ElseClauseSyntax ElseBody(RGParameterInfo param) SyntaxFactory.SingletonSeparatedList( SyntaxFactory.Argument( SyntaxFactory.LiteralExpression( - SyntaxKind.StringLiteralExpression, + SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal($"No parameter '{param.Name}' found") ) ) ) ) ) - ), SyntaxFactory.ReturnStatement() + ), SyntaxFactory.ReturnStatement() } )); } @@ -400,7 +412,7 @@ private static MemberDeclarationSyntax GenerateActionRequestConstructor(RGAction { var methodParameters = new List(); var parameterParsingStatements = new List(); - + foreach (var rgParameterInfo in action.Parameters) { methodParameters.Add(SyntaxFactory.Parameter(SyntaxFactory.Identifier(rgParameterInfo.Name)) @@ -416,7 +428,7 @@ private static MemberDeclarationSyntax GenerateActionRequestConstructor(RGAction } inputString += "\r\n};"; - + parameterParsingStatements.Add( SyntaxFactory.ParseStatement(inputString) ); @@ -435,4 +447,4 @@ private static MemberDeclarationSyntax GenerateActionRequestConstructor(RGAction } #endif -} \ No newline at end of file +} diff --git a/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGActionMapClass.cs b/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGActionMapClass.cs index 5dfb795f3..56bf62bf5 100644 --- a/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGActionMapClass.cs +++ b/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGActionMapClass.cs @@ -3,6 +3,7 @@ using System.Linq; using UnityEngine; #if UNITY_EDITOR +using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -11,7 +12,7 @@ namespace RegressionGames.Editor.CodeGenerators { -#if UNITY_EDITOR +#if UNITY_EDITOR // Dev Note: Not perfect, but mega time saver for generating this gook: https://roslynquoter.azurewebsites.net/ public static class GenerateRGActionMapClass { @@ -37,22 +38,22 @@ public static void Generate(List botActions) usings.Select(v=>SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(v))).ToArray() ) .AddMembers(GenerateClass(botActions)); - - + + // Create a compilation unit and add the namespace declaration CompilationUnitSyntax compilationUnit = SyntaxFactory.CompilationUnit() .AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System"))) .AddMembers(namespaceDeclaration); // Format the generated code - string formattedCode = compilationUnit.NormalizeWhitespace().ToFullString(); + string formattedCode = compilationUnit.NormalizeWhitespace(eol: Environment.NewLine).ToFullString(); // Save to 'Assets/RegressionGames/Runtime/GeneratedScripts/RGActionMap.cs' string fileName = "RGActionMap.cs"; string filePath = Path.Combine(Application.dataPath, "RegressionGames", "Runtime", "GeneratedScripts", fileName); string fileContents = CodeGeneratorUtils.HeaderComment + formattedCode; Directory.CreateDirectory(Path.GetDirectoryName(filePath)); - File.WriteAllText(filePath, fileContents); + File.WriteAllText(filePath, fileContents); RGDebug.Log($"Successfully Generated {filePath}"); } @@ -66,27 +67,27 @@ private static ClassDeclarationSyntax GenerateClass(List .AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)) .WithBody(SyntaxFactory.Block(filteredActions .GroupBy(b => b.Object) - .SelectMany(g => + .SelectMany(g => { - var innerIfStatements = g.Select(b => + var innerIfStatements = g.Select(b => SyntaxFactory.ExpressionStatement( SyntaxFactory.InvocationExpression( SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName("gameObject"), + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("gameObject"), SyntaxFactory.IdentifierName($"AddComponent") ) ) ) ).ToArray(); - - return new StatementSyntax[] + + return new StatementSyntax[] { SyntaxFactory.IfStatement( SyntaxFactory.InvocationExpression( SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.ThisExpression(), + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ThisExpression(), SyntaxFactory.GenericName("TryGetComponent") .WithTypeArgumentList( SyntaxFactory.TypeArgumentList( @@ -98,8 +99,8 @@ private static ClassDeclarationSyntax GenerateClass(List ), SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList( SyntaxFactory.Argument( - null, - SyntaxFactory.Token(SyntaxKind.OutKeyword), + null, + SyntaxFactory.Token(SyntaxKind.OutKeyword), SyntaxFactory.DeclarationExpression( SyntaxFactory.IdentifierName("var"), SyntaxFactory.DiscardDesignation(SyntaxFactory.Token(SyntaxKind.UnderscoreToken)) diff --git a/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGSerializationClass.cs b/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGSerializationClass.cs index 8f9c118a1..e974bb8e3 100644 --- a/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGSerializationClass.cs +++ b/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGSerializationClass.cs @@ -2,6 +2,7 @@ using System.IO; using UnityEngine; #if UNITY_EDITOR +using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -29,7 +30,7 @@ public static void Generate(List botActions) .AddMembers(namespaceDeclaration); // Format the generated code - string formattedCode = compilationUnit.NormalizeWhitespace().ToFullString(); + string formattedCode = compilationUnit.NormalizeWhitespace(eol: Environment.NewLine).ToFullString(); // Save to 'Assets/RegressionGames/Runtime/GeneratedScripts/RGSerialization.cs' string fileName = "RGSerialization.cs"; @@ -52,14 +53,14 @@ private static ClassDeclarationSyntax GenerateClass(List { continue; } - + foreach (RGParameterInfo parameter in botAction.Parameters) { if (RGUtils.IsCSharpPrimitive(parameter.Type)) { continue; } - + if (!processedTypes.Contains(parameter.Type)) { processedTypes.Add(parameter.Type); @@ -114,4 +115,4 @@ private static string GetDeserializerMethodName(RGParameterInfo parameter) } } #endif -} \ No newline at end of file +} diff --git a/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGStateClasses.cs b/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGStateClasses.cs index 393071d8a..5e329b401 100644 --- a/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGStateClasses.cs +++ b/src/gg.regression.unity.bots/Editor/Scripts/CodeGenerators/GenerateRGStateClasses.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using RegressionGames.RGBotConfigs; using UnityEditor; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; #endif @@ -20,7 +21,7 @@ public static class GenerateRGStateClasses { public static void Generate(List rgStateAttributesInfos) { - Dictionary fileWriteTasks = new(); + Dictionary fileWriteTasks = new(); foreach (var rgStateAttributeInfo in rgStateAttributesInfos) { if (rgStateAttributeInfo.ShouldGenerateCSFile) @@ -62,7 +63,7 @@ public static void Generate(List rgStateAttributesInfos) // Create the Start method var startMethod = GenerateStartMethod(componentType, rgStateAttributeInfo.State); - + // Create the SEInstance method var seInstanceMethod = GenerateGetTypeForStateEntityMethod(componentType); @@ -71,6 +72,10 @@ public static void Generate(List rgStateAttributesInfos) // Add the members to the class declaration classDeclaration = classDeclaration + .AddAttributeLists(AttributeList(SeparatedList(new [] { + CodeGeneratorUtils.Attribute(typeof(DisallowMultipleComponent)), + CodeGeneratorUtils.Attribute(typeof(RequireComponent), CodeGeneratorUtils.TypeOf(typeof(RGEntity))) + }))) .AddMembers(fieldDeclaration, startMethod, seInstanceMethod, getStateMethod); // Create namespace @@ -100,12 +105,17 @@ public static void Generate(List rgStateAttributesInfos) GenerateStateEntityFields(rgStateAttributeInfo.State) ), classDeclaration - ); // Add the namespace declaration to the compilation unit compilationUnit = compilationUnit.AddMembers(namespaceDeclaration); + compilationUnit = compilationUnit.AddMembers(CodeGeneratorUtils.CreatePartialAttacherClass( + rgStateAttributeInfo.NameSpace, + componentType, + typeof(RGEntity).FullName, + $"RegressionGames.RGBotConfigs.{className}")); + // Get the full code text var formattedCode = compilationUnit.NormalizeWhitespace().ToFullString(); @@ -116,7 +126,7 @@ public static void Generate(List rgStateAttributesInfos) string filePath = Path.Combine(Application.dataPath, subfolderName, fileName); string fileContents = CodeGeneratorUtils.HeaderComment + formattedCode; Directory.CreateDirectory(Path.GetDirectoryName(filePath)); - + var task= File.WriteAllTextAsync(filePath, fileContents); fileWriteTasks[filePath] = task; } @@ -229,7 +239,7 @@ private static MethodDeclarationSyntax GenerateGetStateMethod(string componentTy // Add statements to add each state variable to the dictionary statements.AddRange(memberInfos.Select(mi => { - ExpressionSyntax valueExpression = mi.FieldType == "method" + ExpressionSyntax valueExpression = mi.FieldType == "method" ? InvocationExpression( MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, @@ -276,9 +286,9 @@ private static MethodDeclarationSyntax GenerateGetStateMethod(string componentTy return getStateMethod; } - - - + + + private static MemberDeclarationSyntax[] GenerateStateEntityFields(List memberInfos) { var fields = new List(); @@ -286,7 +296,7 @@ private static MemberDeclarationSyntax[] GenerateStateEntityFields(List SearchForBotActionAttributes() CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot(); - var botActionMethods = root.DescendantNodes() - .OfType() - .Where(method => - method.AttributeLists.Any(attrList => - attrList.Attributes.Any(attr => - attr.Name.ToString() == "RGAction"))) - .ToList(); - - foreach (var method in botActionMethods) + var classDeclarations = root.DescendantNodes().OfType(); + foreach (var classDeclaration in classDeclarations) { - string nameSpace = method.Ancestors().OfType().FirstOrDefault()?.Name.ToString(); - string className = method.Ancestors().OfType().First().Identifier.ValueText; - string methodName = method.Identifier.ValueText; - - string actionName = methodName; - var actionAttribute = method.AttributeLists.SelectMany(attrList => attrList.Attributes) - .FirstOrDefault(attr => attr.Name.ToString() == "RGAction"); - - var attributeArgument = actionAttribute?.ArgumentList?.Arguments.FirstOrDefault(); - if (attributeArgument is { Expression: LiteralExpressionSyntax literal }) + var className = classDeclaration.Identifier.ValueText; + var nameSpace = classDeclaration.Ancestors().OfType().FirstOrDefault() + ?.Name.ToString(); + var botActionMethods = classDeclaration.Members + .OfType() + .Where(method => + method.AttributeLists.Any(attrList => + attrList.Attributes.Any(attr => + attr.Name.ToString() == "RGAction"))) + .ToList(); + var isPartial = classDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)); + if (botActionMethods.Count > 0 && !isPartial) { - actionName = literal.Token.ValueText; + RecordError( + $"Error: Class '{className}' must be marked with the 'partial' keyword (for example 'public partial class {className}') to use the [RGAction] attribute."); + continue; } - var semanticModel = compilation.GetSemanticModel(syntaxTree); - var parameterList = method.ParameterList.Parameters.Select(parameter => - new RGParameterInfo - { - Name = parameter.Identifier.ValueText, - Type = RemoveGlobalPrefix(semanticModel.GetTypeInfo(parameter.Type).Type - .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), - Nullable = parameter.Type is NullableTypeSyntax || // ex. int? - (parameter.Type is GenericNameSyntax syntax && // ex. Nullable - syntax.Identifier.ValueText == "Nullable") - }).ToList(); - - botActionList.Add(new RGActionAttributeInfo + foreach (var method in botActionMethods) { - // if this wasn't in a sample project folder, we need to generate CS for it - ShouldGenerateCSFile = excludedPaths.All(ep => !csFilePath.StartsWith(ep)), - Namespace = nameSpace, - Object = className, - MethodName = methodName, - ActionName = actionName, - Parameters = parameterList - }); + string methodName = method.Identifier.ValueText; + + string actionName = methodName; + var actionAttribute = method.AttributeLists.SelectMany(attrList => attrList.Attributes) + .FirstOrDefault(attr => attr.Name.ToString() == "RGAction"); + + if (actionAttribute != null) + { + var attributeArgument = actionAttribute.ArgumentList?.Arguments.FirstOrDefault(); + if (attributeArgument != null && + attributeArgument.Expression is LiteralExpressionSyntax literal) + { + actionName = literal.Token.ValueText; + } + } + + var semanticModel = compilation.GetSemanticModel(syntaxTree); + var parameterList = method.ParameterList.Parameters.Select(parameter => + new RGParameterInfo + { + Name = parameter.Identifier.ValueText, + Type = RemoveGlobalPrefix(semanticModel.GetTypeInfo(parameter.Type).Type + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + Nullable = parameter.Type is NullableTypeSyntax || // ex. int? + (parameter.Type is GenericNameSyntax syntax && // ex. Nullable + syntax.Identifier.ValueText == "Nullable") + }).ToList(); + + botActionList.Add(new RGActionAttributeInfo + { + // if this wasn't in a sample project folder, we need to generate CS for it + ShouldGenerateCSFile = excludedPaths.All(ep => !csFilePath.StartsWith(ep)), + Namespace = nameSpace, + Object = className, + MethodName = methodName, + ActionName = actionName, + Parameters = parameterList + }); + } } } @@ -395,14 +412,21 @@ private static List SearchForBotStateAttributes() foreach (var classDeclaration in classDeclarations) { - string nameSpace = classDeclaration.Ancestors().OfType().FirstOrDefault()?.Name.ToString(); string className = classDeclaration.Identifier.ValueText; List stateList = new List(); var membersWithRGState = classDeclaration.Members - .Where(m => m.AttributeLists.Any(a => a.Attributes.Any(attr => attr.Name.ToString() == "RGState"))); + .Where(m => m.AttributeLists.Any(a => a.Attributes.Any(attr => attr.Name.ToString() == "RGState"))) + .ToList(); + var isPartial = classDeclaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)); + if (membersWithRGState.Count > 0 && !isPartial) + { + // The class isn't partial + RecordError($"Error: Class '{className}' must be marked with the 'partial' keyword (for example 'public partial class {className}') to use the [RGState] attribute."); + continue; + } foreach (var member in membersWithRGState) { @@ -450,7 +474,6 @@ private static List SearchForBotStateAttributes() } string fieldType = member is MethodDeclarationSyntax ? "method" : "variable"; - string fieldName = null; string type = null; switch (member) diff --git a/src/gg.regression.unity.bots/Runtime/Scripts/RGBotConfigs/RGState.cs b/src/gg.regression.unity.bots/Runtime/Scripts/RGBotConfigs/RGState.cs index 5a8103299..aa3c7598f 100644 --- a/src/gg.regression.unity.bots/Runtime/Scripts/RGBotConfigs/RGState.cs +++ b/src/gg.regression.unity.bots/Runtime/Scripts/RGBotConfigs/RGState.cs @@ -20,7 +20,7 @@ public abstract class RGState : MonoBehaviour, IRGState // ReSharper disable once InconsistentNaming // we require each state to have an 'RGEntity' component protected RGEntity rgEntity => GetComponent(); - + /** * A function that is overriden to provide the custom state of this specific GameObject. * For example, you may want to retrieve and set the health of a player on the returned @@ -48,7 +48,7 @@ public IRGStateEntity GetGameObjectState() return state; } - + protected virtual Type GetTypeForStateEntity() { return typeof(RGStateEntity); @@ -69,15 +69,15 @@ private IRGStateEntity CreateStateEntityClassInstance() } return (IRGStateEntity)Activator.CreateInstance(type); } - - + + // Used to fill in the core state for any RGEntity that does NOT have an // RGState implementation on its game object public static IRGStateEntity GenerateCoreStateForRGEntity(RGEntity rgEntity) { IRGStateEntity state; - + // if button.. include whether it is interactable var button = rgEntity.Button; if (button != null) @@ -91,7 +91,7 @@ public static IRGStateEntity GenerateCoreStateForRGEntity(RGEntity rgEntity) state = new RGStateEntity(); } var theTransform = rgEntity.transform; - + state["id"] = theTransform.GetInstanceID(); // default to the gameObject name without uniqueness numbers var otName = rgEntity.objectType.Trim(); @@ -119,7 +119,7 @@ public static IRGStateEntity GenerateCoreStateForRGEntity(RGEntity rgEntity) } return state; } - + private static void PopulateEverythingStateForEntity(IRGStateEntity state, RGEntity entity) { var obsoleteAttributeType = typeof(ObsoleteAttribute); @@ -162,4 +162,4 @@ private static void PopulateEverythingStateForEntity(IRGStateEntity state, RGEnt } } -} \ No newline at end of file +}