commit 7eb2e750e91ee9e702b5e8fd72ed32f7f54e998e Author: Mario Steele Date: Fri Sep 26 03:13:44 2025 -0500 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e80f887 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +.qodo/ +bin/ +obj/ diff --git a/ActionGenerator.cs b/ActionGenerator.cs new file mode 100644 index 0000000..1d657eb --- /dev/null +++ b/ActionGenerator.cs @@ -0,0 +1,71 @@ + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.Sharp.Extended.Generators; + +[Generator] +public class ActionGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var projectFile = context.AdditionalTextsProvider + .Where(file => file.Path.EndsWith("project.godot")) + .Select((file, token) => file.GetText()) + .Collect(); + + context.RegisterSourceOutput(projectFile, GenerateCode); + } + + private void GenerateCode(SourceProductionContext ctx, ImmutableArray texts) + { + var memberCode = new StringBuilder(); + foreach (var text in texts) + { + if (text == null) continue; + var content = text.ToString(); + var inputs = GetActions(content); + var fields = inputs.Select(keyData => $$"""\tpublic static StringName {{keyData.ToPascalCase()}} = "{{keyData}}";"""); + + foreach (var field in fields) memberCode.AppendLine(field); + } + + var code = $$""" + // + + using System; + using Godot; + + namespace Godot.Sharp.Extended; + + public partial class Actions + { + {{memberCode}} + } + """; + ctx.AddSource("Godot.Sharp.Extended.Actions.g.cs", SourceText.From(code, Encoding.UTF8)); + } + + public List GetActions(string content) + { + var results = new List(); + var inputSection = false; + foreach (var line in content.Split(['\n'])) + { + if (line.Equals("[input]")) + inputSection = true; + else if (line.StartsWith("[") && inputSection) break; + + if (!inputSection) continue; + + var kv = line.Split(['='], 2); + if (kv.Length == 2) results.Add(kv[0].Trim()); + } + + return results; + } +} \ No newline at end of file diff --git a/Godot.Sharp.Extended.csproj b/Godot.Sharp.Extended.csproj new file mode 100644 index 0000000..8f7100f --- /dev/null +++ b/Godot.Sharp.Extended.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + false + latest + + true + true + + Godot.Sharp.Extended + Godot.Sharp.Extended + Godot.Sharp.Extended + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Godot.Sharp.Extended.sln b/Godot.Sharp.Extended.sln new file mode 100644 index 0000000..4ba4e2c --- /dev/null +++ b/Godot.Sharp.Extended.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.Sharp.Extended", "Godot.Sharp.Extended.csproj", "{4D64B952-274C-14C1-D766-218DC5756B49}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4D64B952-274C-14C1-D766-218DC5756B49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D64B952-274C-14C1-D766-218DC5756B49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D64B952-274C-14C1-D766-218DC5756B49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D64B952-274C-14C1-D766-218DC5756B49}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2935E47E-E0E5-4C65-9EE5-E6945505387D} + EndGlobalSection +EndGlobal diff --git a/NodeBindGenerator.cs b/NodeBindGenerator.cs new file mode 100644 index 0000000..59e4be1 --- /dev/null +++ b/NodeBindGenerator.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.Sharp.Extended.Generators; + +[Generator] +public class NodeBindGenerator : IIncrementalGenerator +{ + private const string AttributeSourceCode = """ + // + namespace Godot.Sharp.Extended; + + [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field)] + public class NodeBindAttribute : Attribute + { + } + """; + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + "NodeBindAttribute.g.cs", + SourceText.From(AttributeSourceCode, Encoding.UTF8))); + + var propertyProvider = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => s is ClassDeclarationSyntax, + transform: (ctx, _) => GetDeclarationForSourceGen(ctx)) + .Where(t => t.attributeFound) + .SelectMany((t, _) => t.Item1); + + context.RegisterSourceOutput(context.CompilationProvider.Combine(propertyProvider.Collect()), + (ctx, value) => GenerateCode(ctx, value.Left, value.Right)); + } + + private (IEnumerable, bool attributeFound) GetDeclarationForSourceGen( + GeneratorSyntaxContext context) + { + var classSyntax = (ClassDeclarationSyntax)context.Node; + var members = classSyntax.Members; + var foundMembers = members.Select(member => IsNodeBindAttributeOnMember(context.SemanticModel, member)) + .Where(syntax => syntax is not null) + .ToList(); + return (foundMembers!, foundMembers.Count > 0); + } + + private MemberDeclarationSyntax? IsNodeBindAttributeOnMember(SemanticModel model, + MemberDeclarationSyntax declarationSyntax) + { + foreach (var attributeSyntax in declarationSyntax + .AttributeLists + .SelectMany(attributeList => attributeList.Attributes)) + { + if (model.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol symbol) + continue; + + var attributeName = symbol.ContainingType.ToDisplayString(); + if (attributeName == $"Godot.Sharp.Extended.NodeBind") + return declarationSyntax; + } + + return null; + } + + private void GenerateCode(SourceProductionContext ctx, Compilation compilation, + ImmutableArray members) + { + Dictionary classDefinitions = []; + + foreach (var member in members) + { + if (member.Parent is ClassDeclarationSyntax classDeclaration) + { + var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol) + continue; + + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var className = classDeclaration.Identifier.Text; + var key = $"{namespaceName}.{className}"; + if (!classDefinitions.ContainsKey(key)) classDefinitions[key] = new ClassDefinition(); + + var classDefinition = classDefinitions[key]; + classDefinition.ClassName = className; + classDefinition.Namespace = namespaceName; + if (member is FieldDeclarationSyntax field) + { + var variable = field.Declaration.Variables.First(); + + if (semanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol) continue; + + var memberDefinition = new MemberDefinition + { + Name = fieldSymbol.Name, + Type = fieldSymbol.Type.Name + }; + classDefinition.MemberDefinitions.Add(memberDefinition); + classDefinition.UsingNamespaceName.Add(fieldSymbol.Type.ContainingNamespace.ToDisplayString()); + } + else if (member is PropertyDeclarationSyntax property) + { + if (semanticModel.GetDeclaredSymbol(property) is not IPropertySymbol propertySymbol) continue; + + var memberDefinition = new MemberDefinition + { + Name = propertySymbol.Name, + Type = propertySymbol.Type.Name + }; + classDefinition.MemberDefinitions.Add(memberDefinition); + classDefinition.UsingNamespaceName.Add(propertySymbol.Type.ContainingNamespace.ToDisplayString()); + } + } + } + + foreach (var definition in classDefinitions.Values) GenerateClass(ctx, definition); + } + + private void GenerateClass(SourceProductionContext ctx, ClassDefinition definition) + { + var memberCode = new StringBuilder(); + foreach (var memberDefinition in definition.MemberDefinitions) + { + var nodeName = memberDefinition.Name.TrimStart('_').ToPascalCase(); + + memberCode.AppendLine($$""" + {{memberDefinition.Name}} = GetNode<{{memberDefinition.Type}}>("%{{nodeName}}"); + """); + } + + var usingCode = new StringBuilder(); + foreach (var namespaceName in definition.UsingNamespaceName) usingCode.AppendLine($"using {namespaceName};"); + + var code = $$""" + // + + using System; + {{usingCode}} + + namespace {{definition.Namespace}}; + + partial class {{definition.ClassName}} + { + private void BindNodes() { + {{memberCode}} + } + } + """; + ctx.AddSource($"Godot.Sharp.Extended.{definition.ClassName}.NodeBind.g.cs", SourceText.From(code, Encoding.UTF8)); + } + + private class ClassDefinition + { + public string Namespace { get; set; } = ""; + public string ClassName { get; set; } = ""; + public HashSet UsingNamespaceName { get; set; } = []; + public List MemberDefinitions { get; } = []; + } + + private class MemberDefinition + { + public string Type { get; set; } = ""; + public string Name { get; set; } = ""; + } +} \ No newline at end of file diff --git a/NodePropBindGenerator.cs b/NodePropBindGenerator.cs new file mode 100644 index 0000000..976fecd --- /dev/null +++ b/NodePropBindGenerator.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Net.Mime; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.Sharp.Extended.Generators; + +[Generator] +public class NodePropBindGenerator : IIncrementalGenerator +{ + private const string AttributeSourceCode = """ + // + namespace Godot.Sharp.Extended; + + [System.AttributeUsage(System.AttributeTargets.Member)] + public class NodePropBindAttribute : System.Attribute + { + public string TargetNodeName { get; private set; } + public string GodotPropertyName { get; private set; } + + public NodePropBindAttribute(string targetNodeName, string godotPropertyName) + { + TargetNodeName = targetNodeName; + GodotPropertyName = godotPropertyName; + } + } + """; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + "NodePropBind.g.cs", + SourceText.From(AttributeSourceCode, Encoding.UTF8))); + + var propertyProvider = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => s is ClassDeclarationSyntax, + transform: (ctx, _) => GetDeclarationForSourceGen(ctx)) + .Where(t => t.attributeFound) + .SelectMany((t, _) => t.members); + + context.RegisterSourceOutput(context.CompilationProvider.Combine(propertyProvider.Collect()), + (ctx, value) => GenerateCode(ctx, value.Left, value.Right)); + } + + private (IEnumerable members, bool attributeFound) GetDeclarationForSourceGen( + GeneratorSyntaxContext context) + { + var classSyntax = (ClassDeclarationSyntax)context.Node; + var members = classSyntax.Members; + var foundMembers = members.Select(member => IsNodePropBindAttributeOnMember(context.SemanticModel, member)) + .Where(syntax => syntax is not null) + .ToList(); + return (foundMembers!, foundMembers.Count > 0); + } + + private MemberDeclarationSyntax? IsNodePropBindAttributeOnMember(SemanticModel model, + MemberDeclarationSyntax declarationSyntax) + { + foreach (var attributeSyntax in declarationSyntax + .AttributeLists + .SelectMany(attributeList => attributeList.Attributes)) + { + if (model.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol symbol) + continue; + + var attributeName = symbol.ContainingType.ToDisplayString(); + if (attributeName == "Godot.Sharp.Extended.NodePropBind") + return declarationSyntax; + } + + return null; + } + + private void GenerateCode(SourceProductionContext context, Compilation compilation, + ImmutableArray members) + { + Dictionary classDefinitions = []; + + foreach (var member in members) + { + if (member.Parent is ClassDeclarationSyntax classDeclaration) + { + var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol) + continue; + + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var className = classDeclaration.Identifier.Text; + var key = $"{namespaceName}.{className}"; + if (!classDefinitions.ContainsKey(key)) + classDefinitions[key] = new ClassDefinition(namespaceName, className, new HashSet(), + new List()); + + var classDefinition = classDefinitions[key]; + if (member is FieldDeclarationSyntax field) + { + var variable = field.Declaration.Variables.First(); + + if (semanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol) continue; + + var attributeData = fieldSymbol.GetAttributes() + .FirstOrDefault(ad => ad.AttributeClass?.ToDisplayString() == "Godot.Sharp.Extended.NodePropBindAttribute"); + + if (attributeData is null) continue; + + var nodeProp = attributeData.ConstructorArguments[0].Value as string; + var godotProp = attributeData.ConstructorArguments[1].Value as string; + + var memberDefinition = new MemberDefinition(fieldSymbol.Type.Name, fieldSymbol.Name, nodeProp, godotProp); + classDefinition.MemberDefinitions.Add(memberDefinition); + classDefinition.UsingNamespaceName.Add(fieldSymbol.ContainingNamespace.ToDisplayString()); + } + } + } + + foreach (var definition in classDefinitions.Values) GenerateClass(context, definition); + } + + private void GenerateClass(SourceProductionContext context, ClassDefinition definition) + { + var propCode = new StringBuilder(); + foreach (var memberDefinition in definition.MemberDefinitions) + { + var propName = memberDefinition.Name.TrimStart('_').ToPascalCase(); + var godotProp = memberDefinition.GodotProp.ToPascalCase(); + + propCode.AppendLine($$""" + public {{memberDefinition.Type}} {{propName}} + { + get => {{memberDefinition.Name}}; + set { + {{memberDefinition.Name}} = value; + if ({{memberDefinition.NodeProp}} != null) + {{memberDefinition.NodeProp}}.{{godotProp}} = value; + } + } + """); + } + + var usingCode = new StringBuilder(); + foreach (var namespaceName in definition.UsingNamespaceName) usingCode.AppendLine($"using {namespaceName};"); + + var code = $$""" + // + + using System; + using Godot; + {{usingCode}} + + namespace {{definition.Namespace}}; + + partial class {{definition.ClassName}} + { + {{propCode}} + } + """; + context.AddSource($"Godot.Sharp.Extended.{definition.ClassName}.NodeBind.g.cs", SourceText.From(code, Encoding.UTF8)); + } + + private class ClassDefinition(string theNamespace, string className, HashSet usingNamespaceName, List memberDefinitions) + { + public string Namespace => theNamespace; + public string ClassName => className; + public HashSet UsingNamespaceName => usingNamespaceName; + public List MemberDefinitions => memberDefinitions; + } + + private class MemberDefinition(string type, string name, string nodeProp, string godotProp) + { + public string Type => type; + public string Name => name; + public string NodeProp => nodeProp; + public string GodotProp => godotProp; + } +} \ No newline at end of file diff --git a/ResourceGenerator.cs b/ResourceGenerator.cs new file mode 100644 index 0000000..be0799e --- /dev/null +++ b/ResourceGenerator.cs @@ -0,0 +1,241 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.Sharp.Extended.Generators; + +[Generator] +public class ResourceGenerator : IIncrementalGenerator +{ + private const string AttributeSourceCode = """ + // + namespace Godot.Sharp.Extended; + + [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field)] + public class ResourcePathAttribute : System.Attribute + { + public string Path { get; private set; } + + public ResourcePathAttribute(string path) { + Path = path; + } + } + """; + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + "ResourcePathAttribute.g.cs", + SourceText.From(AttributeSourceCode, Encoding.UTF8))); + + var classPaths = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => s is ClassDeclarationSyntax syntax, + transform: (ctx, _) => GetClassDeclaration(ctx)) + .Where(t => t.reportAttributeFound) + .Select((t, _) => (t.Item1, t.path)) + .Collect(); + + var provider2 = context.AdditionalTextsProvider.Combine(classPaths) + .Where(CheckResourcePath) + .Select((t1, _) => + { + var classDeclarationForPath = GetClassDeclarationForPath(t1); + return new Storage(t1.Left, classDeclarationForPath.Item1, classDeclarationForPath.path); + }); + + context.RegisterSourceOutput(context.CompilationProvider.Combine(provider2.Collect()), + (ctx, t) => GenerateCode(ctx, t.Left, t.Right)); + } + + private (ClassDeclarationSyntax, string path) GetClassDeclarationForPath((AdditionalText file, + ImmutableArray<(ClassDeclarationSyntax, string path)> ClassDeclarationSyntax) tuple) + { + var realFilePath = tuple.file.Path; + var relativeFilePath = Path.GetDirectoryName(realFilePath)?.Replace('\\', '/'); + return tuple.ClassDeclarationSyntax.First(data => relativeFilePath!.EndsWith(data.path)); + } + + private bool CheckResourcePath( + (AdditionalText file, ImmutableArray<(ClassDeclarationSyntax, string path)> Right) tuple) + { + var realFilePath = tuple.file.Path; + var filePathToCheck = tuple.Right.Select(data => data.path); + var relativeFilePath = Path.GetDirectoryName(realFilePath)?.Replace('\\', '/'); + return filePathToCheck.Any(path => relativeFilePath?.EndsWith(path) ?? false); + } + + private (ClassDeclarationSyntax classDeclarationSyntax, bool reportAttributeFound, string path) GetClassDeclaration( + GeneratorSyntaxContext ctx) + { + var classDeclarationSyntax = (ClassDeclarationSyntax)ctx.Node; + + foreach (var attributeSyntax in classDeclarationSyntax + .AttributeLists + .SelectMany(attributeListSyntax => attributeListSyntax.Attributes)) + { + var symbolInfo = ctx.SemanticModel.GetSymbolInfo(attributeSyntax); + if (symbolInfo.Symbol is not IMethodSymbol attributeSymbol) continue; + if (attributeSymbol.ContainingType.ToDisplayString() != "Godot.Sharp.Extended.ResourcePath") continue; + var parameter = attributeSyntax.ArgumentList; + var pathArgument = parameter?.Arguments[0]; + + var path = ""; + if (pathArgument?.Expression is LiteralExpressionSyntax literal) path = literal.Token.ValueText; + + return (classDeclarationSyntax, true, path); + } + + return (classDeclarationSyntax, false, ""); + } + + private void GenerateCode( + SourceProductionContext context, + Compilation compilation, + IEnumerable storages) + { + Dictionary classDefinitions = new(); + + foreach (var storage in storages) + { + var path = storage.Path; + var text = storage.Text; + var classDeclarationSyntax = storage.ClassDeclarationSyntax; + + var firstLine = text.GetText()?.Lines[0].ToString(); + if (firstLine == null) continue; + var scriptNameMatch = Regex.Match(firstLine, @"script_class=""(.+?)"""); + if (!scriptNameMatch.Success) scriptNameMatch = Regex.Match(firstLine, @"type=""(.+?)"""); + + var scriptName = scriptNameMatch.Groups[1].Value; + var fileName = Path.GetFileNameWithoutExtension(path); + + var scriptNamespaceType = FindClassSymbol(compilation.GlobalNamespace, scriptName); + var scriptNamespace = scriptNamespaceType?.ContainingNamespace?.ToDisplayString(); + + var semanticModel = compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol) continue; + + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var className = classDeclarationSyntax.Identifier.Text; + + var key = $"{namespaceName}.{className}"; + if (!classDefinitions.ContainsKey(key)) + classDefinitions[key] = new ClassDefinition + { + ClassName = className, + NamespaceName = namespaceName, + }; + + var def = classDefinitions[key]; + def.Resources.Add(new ResourceDefinition + { + Type = scriptName, + FieldName = fileName, + FilePath = path + "/" + fileName + ".tres" + }); + + if (scriptNamespace != null) def.UsingDefinitionSet.Add(scriptNamespace); + } + + foreach (var classDefinition in classDefinitions.Values) + { + var resources = new StringBuilder(); + var allDefinition = new StringBuilder(); + var usingDefinition = new StringBuilder(); + + foreach(var def in classDefinition.UsingDefinitionSet) usingDefinition.AppendLine($"using {def};"); + + if (!classDefinition.UsingDefinitionSet.Contains("Godot")) usingDefinition.AppendLine("using Godot;"); + + foreach(var resource in classDefinition.Resources) + resources.AppendLine( + $""" + public static readonly {resource.Type} {resource.FieldName} = + GD.Load<{resource.Type}>(ProjectSettings.GlobalizePath("res://{resource.FilePath}")); + """); + + var resourceType = ""; + var generateAll = true; + foreach (var resource in classDefinition.Resources) + { + if (resourceType != "" && resourceType != resource.Type) generateAll = false; + + resourceType = resource.Type; + allDefinition.AppendLine( + $" {resource.FieldName},"); + } + + var allMethod = ""; + if (generateAll) + allMethod = + $""" + public static {resourceType}[] All => [ + {allDefinition}]; + """; + else + allMethod = + $""" + public static Resource[] All => [ + {allDefinition}]; + """; + + var code = + $$""" + // + using System; + {{usingDefinition}} + + namespace {{classDefinition.NamespaceName}}; + + public partial class {{classDefinition.ClassName}} + { + {{resources}} + {{allMethod}} + } + """; + context.AddSource($"Godot.Sharp.Extended.{classDefinition.ClassName}.Res.g.cs", SourceText.From(code, Encoding.UTF8)); + } + } + + private INamedTypeSymbol? FindClassSymbol(INamespaceSymbol namespaceSymbol, string className) + { + var classSymbol = namespaceSymbol.GetTypeMembers().FirstOrDefault(m => m.Name == className); + if (classSymbol != null) return classSymbol; + + foreach (var childNamespace in namespaceSymbol.GetNamespaceMembers()) + { + classSymbol = FindClassSymbol(childNamespace, className); + if (classSymbol != null) return classSymbol; + } + + return null; + } + + private class ClassDefinition + { + public HashSet UsingDefinitionSet { get; private set; } = []; + public string NamespaceName { get; set; } = ""; + public string ClassName { get; set; } = ""; + public List Resources { get; private set; } = []; + } + + private class ResourceDefinition + { + public string Type { get; set; } = ""; + public string FilePath { get; set; } = ""; + public string FieldName { get; set; } = ""; + } + + private class Storage(AdditionalText text, ClassDeclarationSyntax classDeclarationSyntax, string path) + { + public AdditionalText Text => text; + public ClassDeclarationSyntax ClassDeclarationSyntax => classDeclarationSyntax; + public string Path => path; + } +} \ No newline at end of file diff --git a/SceneGenerator.cs b/SceneGenerator.cs new file mode 100644 index 0000000..b3ad16d --- /dev/null +++ b/SceneGenerator.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; + +namespace Godot.Sharp.Extended.Generators; + +[Generator] +public class SceneGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var scenes = context.AdditionalTextsProvider + .Where(text => text.Path.EndsWith(".tscn")); + + context.RegisterSourceOutput( + context.CompilationProvider.Combine(scenes.Collect()).Combine(context.AnalyzerConfigOptionsProvider), + (ctx, t) => GenerateCode(ctx, t.Left.Left, t.Left.Right, t.Right)); + } + + private void GenerateCode( + SourceProductionContext context, + Compilation compilation, + IEnumerable scenes, + AnalyzerConfigOptionsProvider optionsProvider) + { + optionsProvider.GlobalOptions.TryGetValue("build_property.projectdir", out var projectDirectory); + + var allMethods = new StringBuilder(); + foreach (var scene in scenes) + { + var directory = Path.GetDirectoryName(scene.Path); + var name = Path.GetFileNameWithoutExtension(scene.Path); + var relativePath = scene.Path.Replace(projectDirectory ?? "", "").Replace("\\", "/"); + allMethods.AppendLine( + $$""" + public static T New{{name.ToPascalCase()}}() where T : Node { + var packedScene = GD.Load("res://{{relativePath}}"); + return packedScene.Instance(); + } + """); + + var code = + $$""" + // + + using System; + using Godot; + + namespace Godot.Sharp.Extended; + + public class Scenes + { + {{allMethods}} + } + """; + + context.AddSource("Godot.Sharp.Extended.Scenes.g.cs", SourceText.From(code, Encoding.UTF8)); + } + } + +} \ No newline at end of file diff --git a/StringExtensionMethods.cs b/StringExtensionMethods.cs new file mode 100644 index 0000000..19f63bc --- /dev/null +++ b/StringExtensionMethods.cs @@ -0,0 +1,305 @@ +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace Godot.Sharp.Extended.Generators; + +public static class StringExtensionMethods +{ + public static string ToPascalCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForPascalOrCamelCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + newString[newIndex] = insertSeparator + ? char.ToUpperInvariant(value[i]) + : char.ToLowerInvariant(value[i]); + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToCamelCase(this string value) + { + int newIndex = 0; + bool insertSeparator = false; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForPascalOrCamelCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + newString[newIndex] = insertSeparator && !isFirstCharacter + ? char.ToUpperInvariant(value[i]) + : char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToUnderscoreCamelCase(this string value) + { + int newIndex = 1; + bool insertSeparator = false; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForPascalOrCamelCase(value) + 1]; + newString[0] = '_'; + for (int i = 1; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + newString[newIndex] = insertSeparator && !isFirstCharacter + ? char.ToUpperInvariant(value[i]) + : char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToKebabCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + if (insertSeparator && !isFirstCharacter) + { + newString[newIndex] = '-'; + newIndex++; + } + + newString[newIndex] = char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToSnakeCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + if (insertSeparator && !isFirstCharacter) + { + newString[newIndex] = '_'; + newIndex++; + } + + newString[newIndex] = char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToMacroCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + if (insertSeparator && !isFirstCharacter) + { + newString[newIndex] = '_'; + newIndex++; + } + + newString[newIndex] = char.ToUpperInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToTrainCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + if (insertSeparator && !isFirstCharacter) + { + newString[newIndex] = '-'; + newIndex++; + } + + newString[newIndex] = insertSeparator + ? char.ToUpperInvariant(value[i]) + : char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + public static string ToTitleCase(this string value) + { + int newIndex = 0; + bool insertSeparator = true; + bool isFirstCharacter = true; + UnicodeCategory previous; + UnicodeCategory current = UnicodeCategory.OtherSymbol; + char[] newString = new char[value.Length + CalculateSpanSizeForKebabOrSnakeCase(value)]; + for (int i = 0; i < value.Length; i++) + { + previous = current; + current = char.GetUnicodeCategory(value[i]); + insertSeparator = + (previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber)) || insertSeparator; + if (!IsSpecialCharacter(current)) + { + if (insertSeparator && !isFirstCharacter) + { + newString[newIndex] = ' '; + newIndex++; + } + + newString[newIndex] = insertSeparator + ? char.ToUpperInvariant(value[i]) + : char.ToLowerInvariant(value[i]); + isFirstCharacter = false; + insertSeparator = false; + newIndex++; + } + } + + return new(newString); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CalculateSpanSizeForKebabOrSnakeCase(string text) + { + UnicodeCategory previous = char.GetUnicodeCategory(text[0]); + UnicodeCategory current; + int skips = IsSpecialCharacter(previous) ? 1 : 0; + int divs = 0; + for (int i = 1; i < text.Length; i++) + { + current = char.GetUnicodeCategory(text[i]); + skips += IsSpecialCharacter(current) ? 1 : 0; + divs += previous != current && (current is UnicodeCategory.UppercaseLetter || + current is UnicodeCategory.DecimalDigitNumber) + ? 1 + : 0; + previous = current; + } + + return divs - skips; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CalculateSpanSizeForPascalOrCamelCase(string text) + { + UnicodeCategory current; + int skips = 0; + for (int i = 0; i < text.Length - 1; i++) + { + current = char.GetUnicodeCategory(text[i]); + skips -= IsSpecialCharacter(current) ? 1 : 0; + } + + return skips; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSpecialCharacter(UnicodeCategory category) + { + return category is not UnicodeCategory.UppercaseLetter + and not UnicodeCategory.LowercaseLetter + and not UnicodeCategory.DecimalDigitNumber; + } +} \ No newline at end of file