From 9b2d2c67165a1e0fd1eb73e173267f65c6b67e39 Mon Sep 17 00:00:00 2001 From: Stefan <29021710+Saalvage@users.noreply.github.com> Date: Mon, 24 Nov 2025 06:36:04 +0100 Subject: [PATCH 1/2] Statically typed shader global wrapper (#164) --- Godot 4 Tests/Run.cs | 1 + .../ShaderGlobalsAttributeTests.cs | 49 +++++++ .../ShaderGlobalsAttributeTests.cs.uid | 1 + .../ShaderGlobalsAttributeTests.tscn | 6 + .../Utils/Extensions/ReflectionExtensions.cs | 2 + Godot 4 Tests/project.godot | 119 +++++++++++++++ .../ShaderGlobalsExtensions/Resources.cs | 9 ++ .../ShaderGlobalsAttribute.cs | 9 ++ .../ShaderGlobalsDataModel.cs | 137 ++++++++++++++++++ .../ShaderGlobalsScraper.cs | 82 +++++++++++ .../ShaderGlobalsSourceGenerator.cs | 26 ++++ .../ShaderGlobalsTemplate.scriban | 25 ++++ 12 files changed, 466 insertions(+) create mode 100644 Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs create mode 100644 Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid create mode 100644 Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.tscn create mode 100644 SourceGenerators/ShaderGlobalsExtensions/Resources.cs create mode 100644 SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs create mode 100644 SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs create mode 100644 SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs create mode 100644 SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs create mode 100644 SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban diff --git a/Godot 4 Tests/Run.cs b/Godot 4 Tests/Run.cs index 13482f4..c9e204e 100644 --- a/Godot 4 Tests/Run.cs +++ b/Godot 4 Tests/Run.cs @@ -65,6 +65,7 @@ private static IEnumerable> Tests yield return ITest.GetTest; yield return ITest.GetTest; yield return ITest.GetTest; + yield return ITest.GetTest; yield return ITest.GetTest; yield return ITest.GetTest; yield return ITest.GetTest; diff --git a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs new file mode 100644 index 0000000..743e337 --- /dev/null +++ b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs @@ -0,0 +1,49 @@ +using FluentAssertions; +using Godot; +using GodotSharp.BuildingBlocks.TestRunner; + +namespace GodotTests.TestScenes; + +[ShaderGlobals] +public static partial class ShaderGlobals; + +[SceneTree] +public partial class ShaderGlobalsAttributeTests : Node, ITest +{ + void ITest.InitTests() + { + ShaderGlobals.A.Should().BeTrue(); + ShaderGlobals.B.Should().Be(2); + ShaderGlobals.C.Should().Be(0); + ShaderGlobals.D.Should().Be(9); + ShaderGlobals.E.Should().Be(875); + ShaderGlobals.F.Should().Be(new Vector2I(565, 0)); + ShaderGlobals.G.Should().Be(new Vector3I(0, 410, 0)); + ShaderGlobals.H.Should().Be(new Vector4I(0, 475, 0, 180)); + ShaderGlobals.I.Should().Be(new Rect2I(50, 0, 145, 0)); + ShaderGlobals.J.Should().Be(345); + ShaderGlobals.K.Should().Be(new Vector2I(295, 355)); + ShaderGlobals.L.Should().Be(new Vector3I(0, 195, 0)); + ShaderGlobals.M.Should().Be(new Vector4I(0, 0, 275, 0)); + ShaderGlobals.N.Should().Be(0.205f); + ShaderGlobals.O.Should().Be(new Vector2(0.23f, 0.385f)); + ShaderGlobals.P.Should().Be(new Vector3(0.0f, 0.435f, 0.0f)); + ShaderGlobals.Q.Should().Be(new Vector4(0.22f, 0.0f, 0.275f, 0.0f)); + ShaderGlobals.R.Should().Be(new Color(0.517647f, 0.921569f, 0.52549f, 0.788235f)); + ShaderGlobals.S.Should().Be(new Rect2(0.19f, 0.0f, 0.065f, 0.1f)); + ShaderGlobals.T.Should().Be(new Vector4(1.36f, 0.44f, 0.22f, 1.0f)); + ShaderGlobals.U.Should().Be(new Basis(1.0f, 0.205f, 1.37f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f)); + ShaderGlobals.V.Should().Be(new Projection(1.0f, 0.46f, 0.92f, 0.0f, 0.0f, 1.315f, 0.92f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f)); + ShaderGlobals.W.Should().Be(new Transform2D(1.0f, 0.0f, 1.885f, 1.0f, 0.755f, 0.0f)); + ShaderGlobals.X.Should().Be(new Transform3D(1.0f, 0.0f, 0.0f, 0.0f, 1.35f, 0.59f, 0.0f, 0.0f, 1.0f, 0.0f, 0.54f, 0.0f)); + ShaderGlobals.Y.Should().BeNull(); + ShaderGlobals.Y.GetDeclaredType().Should().Be(typeof(Texture2D)); + ShaderGlobals.Z.Should().BeNull(); + ShaderGlobals.Z.GetDeclaredType().Should().Be(typeof(Texture2DArray)); + ShaderGlobals.Ä.Should().Be(GD.Load("res://TestScenes/Feature.ShaderGlobals/Noise.tres")); + ShaderGlobals.Ö.Should().BeNull(); + ShaderGlobals.Ö.GetDeclaredType().Should().Be(typeof(Cubemap)); + ShaderGlobals.Ü.Should().BeNull(); + ShaderGlobals.Ü.GetDeclaredType().Should().Be(typeof(ExternalTexture)); + } +} diff --git a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid new file mode 100644 index 0000000..5bd4640 --- /dev/null +++ b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid @@ -0,0 +1 @@ +uid://dm8vgm0v4e6hi diff --git a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.tscn b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.tscn new file mode 100644 index 0000000..9c3d263 --- /dev/null +++ b/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://qlum4js31ian"] + +[ext_resource type="Script" uid="uid://dm8vgm0v4e6hi" path="res://TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs" id="1_y0unj"] + +[node name="ShaderGlobalsAttributeTests" type="Node"] +script = ExtResource("1_y0unj") diff --git a/Godot 4 Tests/Utils/Extensions/ReflectionExtensions.cs b/Godot 4 Tests/Utils/Extensions/ReflectionExtensions.cs index 6138bb6..5fd9107 100644 --- a/Godot 4 Tests/Utils/Extensions/ReflectionExtensions.cs +++ b/Godot 4 Tests/Utils/Extensions/ReflectionExtensions.cs @@ -45,4 +45,6 @@ public static void ShouldContain(this Type t, if (Properties is not null) t.Properties().Should().Contain(Properties); if (NestedTypes is not null) t.NestedTypes().Should().Contain(NestedTypes); } + + public static Type GetDeclaredType(this T t) => typeof(T); } diff --git a/Godot 4 Tests/project.godot b/Godot 4 Tests/project.godot index 2cae71b..446a33c 100644 --- a/Godot 4 Tests/project.godot +++ b/Godot 4 Tests/project.godot @@ -164,3 +164,122 @@ avoidance/layer_16="- With Leading - 16" avoidance/layer_17="7 With Leading Numeric 17" avoidance/layer_18=". With Leading . 18" avoidance/layer_19="中文 With Leading Unicode 19" + +[shader_globals] + +a={ +"type": "bool", +"value": true +} +b={ +"type": "bvec2", +"value": 2 +} +c={ +"type": "bvec3", +"value": 0 +} +d={ +"type": "bvec4", +"value": 9 +} +e={ +"type": "int", +"value": 875 +} +f={ +"type": "ivec2", +"value": Vector2i(565, 0) +} +g={ +"type": "ivec3", +"value": Vector3i(0, 410, 0) +} +h={ +"type": "ivec4", +"value": Vector4i(0, 475, 0, 180) +} +i={ +"type": "rect2i", +"value": Rect2i(50, 0, 145, 0) +} +j={ +"type": "uint", +"value": 345 +} +k={ +"type": "uvec2", +"value": Vector2i(295, 355) +} +l={ +"type": "uvec3", +"value": Vector3i(0, 195, 0) +} +m={ +"type": "uvec4", +"value": Vector4i(0, 0, 275, 0) +} +n={ +"type": "float", +"value": 0.205 +} +o={ +"type": "vec2", +"value": Vector2(0.23, 0.385) +} +p={ +"type": "vec3", +"value": Vector3(0, 0.435, 0) +} +q={ +"type": "vec4", +"value": Vector4(0.22, 0, 0.275, 0) +} +r={ +"type": "color", +"value": Color(0.517647, 0.921569, 0.52549, 0.788235) +} +s={ +"type": "rect2", +"value": Rect2(0.19, 0, 0.065, 0.1) +} +t={ +"type": "mat2", +"value": PackedFloat32Array(1.36, 0.44, 0.22, 1) +} +u={ +"type": "mat3", +"value": Basis(1, 0.205, 1.37, 0, 1, 0, 0, 0, 1) +} +v={ +"type": "mat4", +"value": Projection(1, 0.46, 0.92, 0, 0, 1.315, 0.92, 0, 0, 0, 1, 0, 0, 0, 0, 1) +} +w={ +"type": "transform_2d", +"value": Transform2D(1, 0, 1.885, 1, 0.755, 0) +} +x={ +"type": "transform", +"value": Transform3D(1, 0, 0, 0, 1.35, 0.59, 0, 0, 1, 0, 0.54, 0) +} +y={ +"type": "sampler2D", +"value": "" +} +z={ +"type": "sampler2DArray", +"value": "" +} +"ä"={ +"type": "sampler3D", +"value": "res://TestScenes/Feature.ShaderGlobals/Noise.tres" +} +"ö"={ +"type": "samplerCube", +"value": "" +} +"ü"={ +"type": "samplerExternalOES", +"value": "" +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/Resources.cs b/SourceGenerators/ShaderGlobalsExtensions/Resources.cs new file mode 100644 index 0000000..1b70b16 --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/Resources.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions; + +internal static class Resources +{ + private const string shaderGlobalsTemplate = "GodotSharp.SourceGenerators.ShaderGlobalsExtensions.ShaderGlobalsTemplate.scriban"; + public static readonly string ShaderGlobalsTemplate = Assembly.GetExecutingAssembly().GetEmbeddedResource(shaderGlobalsTemplate); +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs new file mode 100644 index 0000000..686a96b --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs @@ -0,0 +1,9 @@ +using System.Runtime.CompilerServices; + +namespace Godot; + +[AttributeUsage(AttributeTargets.Class)] +public sealed class ShaderGlobalsAttribute([CallerFilePath] string classPath = null) : Attribute +{ + public string ClassPath { get; } = classPath; +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs new file mode 100644 index 0000000..f74d8ce --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs @@ -0,0 +1,137 @@ +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; + +namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions; + +internal class ShaderGlobalsDataModel : ClassDataModel +{ + public abstract record ShaderGlobalDefault + { + public abstract string GetDefault(string type); + } + + public record ShaderGlobalResourceDefault(string ResourcePath) : ShaderGlobalDefault + { + private static readonly Regex ResourcePathRegex = new(@"^""res://.+""$"); + public static bool TryParse(string @default, out ShaderGlobalDefault outDefault) + { + if (ResourcePathRegex.IsMatch(@default)) + { + outDefault = new ShaderGlobalResourceDefault(@default); + return true; + } + + outDefault = null; + return false; + } + + public override string GetDefault(string type) => $"GD.Load<{type}>({ResourcePath})"; + } + + public record ShaderGlobalConstructorDefault(string Parameters) : ShaderGlobalDefault + { + private static readonly Regex ConstructorRegex = new(@"^.+\((?.+)\)$"); + public static bool TryParse(string @default, out ShaderGlobalDefault outDefault) + { + var match = ConstructorRegex.Match(@default); + if (match.Success) + { + outDefault = new ShaderGlobalConstructorDefault(match.Groups["Parameters"].Value); + return true; + } + + outDefault = null; + return false; + } + + public override string GetDefault(string type) => $"new {type}({string.Join(",", Parameters + .Split(',') + .Select(ConvertFloatingLiteral))})"; + } + + public record ShaderGlobalLiteralDefault(string Literal) : ShaderGlobalDefault + { + public override string GetDefault(string type) => ConvertFloatingLiteral(Literal); + } + + // We need to append 'f' to float literals. + private static string ConvertFloatingLiteral(string literal) => literal.Contains('.') ? literal + "f" : literal; + + public record ShaderGlobal(string Type, string Name, string Default, string RawName); + + public IList ShaderGlobals { get; } + + public ShaderGlobalsDataModel(INamedTypeSymbol symbol, string csPath, string gdRoot) : base(symbol) + { + ShaderGlobals = ShaderGlobalsScraper + .GetShaderGlobals(csPath, gdRoot) + .Select(Convert) + .ToArray(); + + static ShaderGlobal Convert(ShaderGlobalsScraper.ShaderGlobal global) + { + var csType = ConvertType(global.Type); + return new(csType, global.Name.ToPascalCase(), ConvertDefault(global.Default)?.GetDefault(csType) ?? "default", global.Name); + } + + static ShaderGlobalDefault ConvertDefault(string @default) + => @default is null or "" or "\"\"" + ? null + : ShaderGlobalResourceDefault.TryParse(@default, out var outDefault) + ? outDefault + : ShaderGlobalConstructorDefault.TryParse(@default, out outDefault) + ? outDefault + : new ShaderGlobalLiteralDefault(@default); + + static string ConvertType(string type) + => type switch + { + "bvec2" => "int", + "bvec3" => "int", + "bvec4" => "int", + + "ivec2" => "Vector2I", + "ivec3" => "Vector3I", + "ivec4" => "Vector4I", + + "uvec2" => "Vector2I", + "uvec3" => "Vector3I", + "uvec4" => "Vector4I", + + "vec2" => "Vector2", + "vec3" => "Vector3", + "vec4" => "Vector4", + + "color" => "Color", + + "rect2" => "Rect2", + "rect2i" => "Rect2I", + + "mat2" => "Vector4", + "mat3" => "Basis", + "mat4" => "Projection", + + "transform_2d" => "Transform2D", + "transform" => "Transform3D", + + "sampler2D" => "Texture2D", + "sampler2DArray" => "Texture2DArray", + "sampler3D" => "Texture3D", + "samplerCube" => "Cubemap", + "samplerExternalOES" => "ExternalTexture", + + _ => type, + }; + } + + protected override string Str() + { + return string.Join("\n", ShaderGlobals()); + + IEnumerable ShaderGlobals() + { + foreach (var (type, name, @default, rawType) in this.ShaderGlobals) + yield return $"Type: {type}, Name: {name}, Default: {@default}, RawType: {rawType}"; + } + } +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs new file mode 100644 index 0000000..88064e7 --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs @@ -0,0 +1,82 @@ +using System.Text.RegularExpressions; + +namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions; + +internal static class ShaderGlobalsScraper +{ + private static readonly Regex BeginGlobalRegex = new(@"^""?(?.+?)""?={$"); + private static readonly Regex EndGlobalRegex = new("^}$"); + private static readonly Regex TypeRegex = new(@"^""type"": ""(?.+)"",?$"); + private static readonly Regex DefaultRegex = new(@"^""value"": (?.+?),?$"); + + public record ShaderGlobal(string Type, string Name, string Default); + + public static IEnumerable GetShaderGlobals(string csFile, string gdRoot) + { + var gdFile = GD.GetProjectFile(csFile, gdRoot); + Log.Debug($"Scraping {gdFile} [Compiling {csFile}]"); + + return MatchShaderConstants(gdFile); + + static IEnumerable MatchShaderConstants(string gdFile) + { + var found = false; + string name = null; + string type = null; + string @default = null; + foreach (var line in File.ReadLines(gdFile).Where(line => line != string.Empty)) + { + Log.Debug($"Line: {line}"); + + + if (line is "[shader_globals]") + { + found = true; + continue; + } + + if (found) + { + if (name != null) + { + if (EndGlobalRegex.IsMatch(line)) + { + if (type is null) + { + Log.Warn($" - Ignoring shader global without type {name}"); + continue; + } + + yield return new(type, name, @default); + + name = null; + type = null; + @default = null; + } + else if (TypeRegex.Match(line) is { Success: true } typeMatch) + { + if (type != null) + Log.Warn($" - Duplicate type {type} for shader global {name}"); + + type = typeMatch.Groups["Type"].Value; + } + else if (DefaultRegex.Match(line) is { Success: true } defaultMatch) + { + if (@default != null) + Log.Warn($" - Duplicate default {@defaultMatch} for shader global {name}"); + + @default = defaultMatch.Groups["Default"].Value; + } + } + else + { + if (line.StartsWith("[")) + yield break; + else if (BeginGlobalRegex.Match(line) is { Success: true } beginMatch) + name = beginMatch.Groups["Name"].Value; + } + } + } + } + } +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs new file mode 100644 index 0000000..5e3ef5d --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Scriban; + +namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions; + +[Generator] +internal class ShaderGlobalsSourceGenerator : SourceGeneratorForDeclaredTypeWithAttribute +{ + private static Template ShaderGlobalsTemplate => field ??= Template.Parse(Resources.ShaderGlobalsTemplate); + + protected override (string GeneratedCode, DiagnosticDetail Error) GenerateCode(Compilation compilation, SyntaxNode node, INamedTypeSymbol symbol, AttributeData attribute, AnalyzerConfigOptions options) + { + var data = ReconstructAttribute(); + var model = new ShaderGlobalsDataModel(symbol, data.ClassPath, options.TryGetGodotProjectDir()); + Log.Debug($"--- MODEL ---\n{model}\n"); + + var output = ShaderGlobalsTemplate.Render(model, member => member.Name); + Log.Debug($"--- OUTPUT ---\n{output}\n"); + + return (output, null); + + Godot.ShaderGlobalsAttribute ReconstructAttribute() => new( + (string)attribute.ConstructorArguments[0].Value); + } +} diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban new file mode 100644 index 0000000..abc02e7 --- /dev/null +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel; + +using Godot; + +{{~ Namespace ~}} + +partial class {{ClassName}} +{ +{{~ for global in ShaderGlobals ~}} + private static readonly StringName _{{global.Name}}Name = "{{global.RawName}}"; + private static {{global.Type}} _{{global.Name}} = {{global.Default}}; + /// A statically typed wrapper for the shader global {{global.RawName}} defined in godot.project. + /// When the shader global is modified outside of this property, the change will not be reflected in the property. + public static {{global.Type}} {{global.Name}} + { + get => _{{global.Name}}; + set + { + RenderingServer.GlobalShaderParameterSet(_{{global.Name}}Name, value); + _{{global.Name}} = value; + } + } +{{~ end ~}} +} From a5b2bdd5c573f7bfee80ed1a935fea09b2ab27b8 Mon Sep 17 00:00:00 2001 From: Cat-Lips <57929251+Cat-Lips@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:26:41 +1100 Subject: [PATCH 2/2] 164: Exposing default values for shader globals + minor changes/code cleanup --- .editorconfig | 3 +- .../CustomGeneratorTests.csproj | 2 +- Godot 3 Tests/Godot 3 Tests.csproj | 2 +- Godot 4 Tests/Godot 4 Tests.csproj | 2 +- .../RpcExtensionTests.cs | 14 +- .../Feature164.ShaderGlobals/Noise.tres | 6 + .../ShaderGlobalsAttributeTests.cs | 18 +- .../ShaderGlobalsAttributeTests.cs.uid | 0 .../ShaderGlobalsAttributeTests.tscn | 2 +- Godot 4 Tests/project.godot | 2 +- .../ShaderGlobalsAttribute.cs | 9 +- .../ShaderGlobalsDataModel.cs | 173 +++++++----------- .../ShaderGlobalsScraper.cs | 137 +++++++++----- .../ShaderGlobalsSourceGenerator.cs | 8 +- .../ShaderGlobalsTemplate.scriban | 51 ++++-- .../Utilities/Extensions/GodotExtensions.cs | 11 +- .../Utilities/Extensions/StringExtensions.cs | 12 +- 17 files changed, 240 insertions(+), 212 deletions(-) create mode 100644 Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/Noise.tres rename Godot 4 Tests/TestScenes/{Feature.ShaderGlobals => Feature164.ShaderGlobals}/ShaderGlobalsAttributeTests.cs (81%) rename Godot 4 Tests/TestScenes/{Feature.ShaderGlobals => Feature164.ShaderGlobals}/ShaderGlobalsAttributeTests.cs.uid (100%) rename Godot 4 Tests/TestScenes/{Feature.ShaderGlobals => Feature164.ShaderGlobals}/ShaderGlobalsAttributeTests.tscn (71%) diff --git a/.editorconfig b/.editorconfig index 68baf5f..e9c0257 100644 --- a/.editorconfig +++ b/.editorconfig @@ -83,7 +83,7 @@ dotnet_remove_unnecessary_suppression_exclusions = none # New line preferences dotnet_style_allow_multiple_blank_lines_experimental = false -dotnet_style_allow_statement_immediately_after_block_experimental = false +dotnet_style_allow_statement_immediately_after_block_experimental = true #### C# Coding Conventions #### @@ -127,6 +127,7 @@ csharp_prefer_system_threading_lock = true csharp_style_namespace_declarations = file_scoped csharp_style_prefer_method_group_conversion = true csharp_style_prefer_primary_constructors = true +csharp_style_prefer_simple_property_accessors = true csharp_style_prefer_top_level_statements = true # Expression-level preferences diff --git a/CustomGeneratorTests/CustomGeneratorTests.csproj b/CustomGeneratorTests/CustomGeneratorTests.csproj index c00eac3..a1152a4 100644 --- a/CustomGeneratorTests/CustomGeneratorTests.csproj +++ b/CustomGeneratorTests/CustomGeneratorTests.csproj @@ -7,7 +7,7 @@ true - + diff --git a/Godot 3 Tests/Godot 3 Tests.csproj b/Godot 3 Tests/Godot 3 Tests.csproj index 4a80e56..7afe95e 100644 --- a/Godot 3 Tests/Godot 3 Tests.csproj +++ b/Godot 3 Tests/Godot 3 Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Godot 4 Tests/Godot 4 Tests.csproj b/Godot 4 Tests/Godot 4 Tests.csproj index 940cdd2..f5d75e4 100644 --- a/Godot 4 Tests/Godot 4 Tests.csproj +++ b/Godot 4 Tests/Godot 4 Tests.csproj @@ -14,7 +14,7 @@ - + \ No newline at end of file diff --git a/Godot 4 Tests/TestScenes/Feature132.RpcExtensions/RpcExtensionTests.cs b/Godot 4 Tests/TestScenes/Feature132.RpcExtensions/RpcExtensionTests.cs index f7fde35..e63baae 100644 --- a/Godot 4 Tests/TestScenes/Feature132.RpcExtensions/RpcExtensionTests.cs +++ b/Godot 4 Tests/TestScenes/Feature132.RpcExtensions/RpcExtensionTests.cs @@ -71,15 +71,15 @@ void ITest.ReadyTests() Test(() => My2RpcId(1, 2, 2.2f), "2|2|2.2"); Test(() => My3RpcId(1, 3, 3.3f, EnumTest.C), "3|3|3.3|C"); Test(() => My4RpcId(1), "4|4|4.4|D"); - Test(() => MyXRpcId(1), null, ok: false); // Godot logs error if calling self when CallLocal is false + //Test(() => MyXRpcId(1), null, ok: false); // Godot logs error if calling self when CallLocal is false // Godot logs errors for unknown peer id (but doesn't return error?!?!?) - Test(() => My0RpcId(2), null/*, ok: false*/); - Test(() => My1RpcId(2, 1), null/*, ok: false*/); - Test(() => My2RpcId(2, 2, 2.2f), null/*, ok: false*/); - Test(() => My3RpcId(2, 3, 3.3f, EnumTest.C), null/*, ok: false*/); - Test(() => My4RpcId(2), null/*, ok: false*/); - Test(() => MyXRpcId(2), null/*, ok: false*/); + //Test(() => My0RpcId(2), null/*, ok: false*/); + //Test(() => My1RpcId(2, 1), null/*, ok: false*/); + //Test(() => My2RpcId(2, 2, 2.2f), null/*, ok: false*/); + //Test(() => My3RpcId(2, 3, 3.3f, EnumTest.C), null/*, ok: false*/); + //Test(() => My4RpcId(2), null/*, ok: false*/); + //Test(() => MyXRpcId(2), null/*, ok: false*/); void Test(Func sut, string expected, bool ok = true, [CallerArgumentExpression(nameof(sut))] string test = null) { diff --git a/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/Noise.tres b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/Noise.tres new file mode 100644 index 0000000..9cf4817 --- /dev/null +++ b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/Noise.tres @@ -0,0 +1,6 @@ +[gd_resource type="NoiseTexture3D" load_steps=2 format=3 uid="uid://8ff1k1aycfdw"] + +[sub_resource type="FastNoiseLite" id="FastNoiseLite_lids6"] + +[resource] +noise = SubResource("FastNoiseLite_lids6") diff --git a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs similarity index 81% rename from Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs rename to Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs index 743e337..28ea619 100644 --- a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs +++ b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs @@ -40,10 +40,26 @@ void ITest.InitTests() ShaderGlobals.Y.GetDeclaredType().Should().Be(typeof(Texture2D)); ShaderGlobals.Z.Should().BeNull(); ShaderGlobals.Z.GetDeclaredType().Should().Be(typeof(Texture2DArray)); - ShaderGlobals.Ä.Should().Be(GD.Load("res://TestScenes/Feature.ShaderGlobals/Noise.tres")); + ShaderGlobals.Ä.Should().Be(GD.Load("res://TestScenes/Feature164.ShaderGlobals/Noise.tres")); ShaderGlobals.Ö.Should().BeNull(); ShaderGlobals.Ö.GetDeclaredType().Should().Be(typeof(Cubemap)); ShaderGlobals.Ü.Should().BeNull(); ShaderGlobals.Ü.GetDeclaredType().Should().Be(typeof(ExternalTexture)); + + ShaderGlobals.A = false; + ShaderGlobals.B = 7; + ShaderGlobals.N = .777f; + + ShaderGlobals.A.Should().BeFalse(); + ShaderGlobals.B.Should().Be(7); + ShaderGlobals.N.Should().Be(.777f); + + ShaderGlobals.A = ShaderGlobals.Default.A; + ShaderGlobals.B = ShaderGlobals.Default.B; + ShaderGlobals.N = ShaderGlobals.Default.N; + + ShaderGlobals.A.Should().BeTrue(); + ShaderGlobals.B.Should().Be(2); + ShaderGlobals.N.Should().Be(.205f); } } diff --git a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid similarity index 100% rename from Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid rename to Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid diff --git a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.tscn b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.tscn similarity index 71% rename from Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.tscn rename to Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.tscn index 9c3d263..80f2b30 100644 --- a/Godot 4 Tests/TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.tscn +++ b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://qlum4js31ian"] -[ext_resource type="Script" uid="uid://dm8vgm0v4e6hi" path="res://TestScenes/Feature.ShaderGlobals/ShaderGlobalsAttributeTests.cs" id="1_y0unj"] +[ext_resource type="Script" uid="uid://dm8vgm0v4e6hi" path="res://TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs" id="1_y0unj"] [node name="ShaderGlobalsAttributeTests" type="Node"] script = ExtResource("1_y0unj") diff --git a/Godot 4 Tests/project.godot b/Godot 4 Tests/project.godot index 446a33c..23688f9 100644 --- a/Godot 4 Tests/project.godot +++ b/Godot 4 Tests/project.godot @@ -273,7 +273,7 @@ z={ } "ä"={ "type": "sampler3D", -"value": "res://TestScenes/Feature.ShaderGlobals/Noise.tres" +"value": "res://TestScenes/Feature164.ShaderGlobals/Noise.tres" } "ö"={ "type": "samplerCube", diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs index 686a96b..779f21f 100644 --- a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs @@ -1,9 +1,4 @@ -using System.Runtime.CompilerServices; - -namespace Godot; +namespace Godot; [AttributeUsage(AttributeTargets.Class)] -public sealed class ShaderGlobalsAttribute([CallerFilePath] string classPath = null) : Attribute -{ - public string ClassPath { get; } = classPath; -} +public sealed class ShaderGlobalsAttribute() : Attribute; diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs index f74d8ce..f9398e9 100644 --- a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs @@ -1,137 +1,88 @@ -using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions; -internal class ShaderGlobalsDataModel : ClassDataModel +internal class ShaderGlobalsDataModel(INamedTypeSymbol symbol, string gdRoot) : ClassDataModel(symbol) { - public abstract record ShaderGlobalDefault - { - public abstract string GetDefault(string type); - } + public record ShaderGlobal(string Name, string Type, string Default, string RawName); - public record ShaderGlobalResourceDefault(string ResourcePath) : ShaderGlobalDefault - { - private static readonly Regex ResourcePathRegex = new(@"^""res://.+""$"); - public static bool TryParse(string @default, out ShaderGlobalDefault outDefault) - { - if (ResourcePathRegex.IsMatch(@default)) - { - outDefault = new ShaderGlobalResourceDefault(@default); - return true; - } + public IList ShaderGlobals { get; } = [.. + ShaderGlobalsScraper + .GetShaderGlobals(gdRoot) + .Select(Convert)]; - outDefault = null; - return false; - } + protected override string Str() + => string.Join("\n", ShaderGlobals); - public override string GetDefault(string type) => $"GD.Load<{type}>({ResourcePath})"; - } + #region Convert - public record ShaderGlobalConstructorDefault(string Parameters) : ShaderGlobalDefault + private static ShaderGlobal Convert(ShaderGlobalsScraper.ShaderGlobal raw) { - private static readonly Regex ConstructorRegex = new(@"^.+\((?.+)\)$"); - public static bool TryParse(string @default, out ShaderGlobalDefault outDefault) + var csType = ConvertType(raw.Type); + var csValue = ConvertValue(raw.Default) ?? "default"; + return new(raw.Name.ToPascalCase(), csType, csValue, raw.Name); + + string ConvertType(string type) => type switch { - var match = ConstructorRegex.Match(@default); - if (match.Success) - { - outDefault = new ShaderGlobalConstructorDefault(match.Groups["Parameters"].Value); - return true; - } + "bvec2" => "int", + "bvec3" => "int", + "bvec4" => "int", - outDefault = null; - return false; - } + "ivec2" => "Vector2I", + "ivec3" => "Vector3I", + "ivec4" => "Vector4I", - public override string GetDefault(string type) => $"new {type}({string.Join(",", Parameters - .Split(',') - .Select(ConvertFloatingLiteral))})"; - } + "uvec2" => "Vector2I", + "uvec3" => "Vector3I", + "uvec4" => "Vector4I", - public record ShaderGlobalLiteralDefault(string Literal) : ShaderGlobalDefault - { - public override string GetDefault(string type) => ConvertFloatingLiteral(Literal); - } + "vec2" => "Vector2", + "vec3" => "Vector3", + "vec4" => "Vector4", - // We need to append 'f' to float literals. - private static string ConvertFloatingLiteral(string literal) => literal.Contains('.') ? literal + "f" : literal; + "color" => "Color", - public record ShaderGlobal(string Type, string Name, string Default, string RawName); + "rect2" => "Rect2", + "rect2i" => "Rect2I", - public IList ShaderGlobals { get; } + "mat2" => "Vector4", + "mat3" => "Basis", + "mat4" => "Projection", - public ShaderGlobalsDataModel(INamedTypeSymbol symbol, string csPath, string gdRoot) : base(symbol) - { - ShaderGlobals = ShaderGlobalsScraper - .GetShaderGlobals(csPath, gdRoot) - .Select(Convert) - .ToArray(); + "transform_2d" => "Transform2D", + "transform" => "Transform3D", + + "sampler2D" => "Texture2D", + "sampler2DArray" => "Texture2DArray", + "sampler3D" => "Texture3D", + "samplerCube" => "Cubemap", + "samplerExternalOES" => "ExternalTexture", - static ShaderGlobal Convert(ShaderGlobalsScraper.ShaderGlobal global) + _ => type, + }; + + string ConvertValue(string v) { - var csType = ConvertType(global.Type); - return new(csType, global.Name.ToPascalCase(), ConvertDefault(global.Default)?.GetDefault(csType) ?? "default", global.Name); - } + return v is null ? null : TryAsRes() ?? TryAsCtor() ?? SafeValue(v); + + string TryAsRes() + => v.StartsWith("res://") ? @$"GD.Load<{csType}>(""{v}"")" : null; - static ShaderGlobalDefault ConvertDefault(string @default) - => @default is null or "" or "\"\"" - ? null - : ShaderGlobalResourceDefault.TryParse(@default, out var outDefault) - ? outDefault - : ShaderGlobalConstructorDefault.TryParse(@default, out outDefault) - ? outDefault - : new ShaderGlobalLiteralDefault(@default); - - static string ConvertType(string type) - => type switch + string TryAsCtor() { - "bvec2" => "int", - "bvec3" => "int", - "bvec4" => "int", - - "ivec2" => "Vector2I", - "ivec3" => "Vector3I", - "ivec4" => "Vector4I", - - "uvec2" => "Vector2I", - "uvec3" => "Vector3I", - "uvec4" => "Vector4I", - - "vec2" => "Vector2", - "vec3" => "Vector3", - "vec4" => "Vector4", - - "color" => "Color", - - "rect2" => "Rect2", - "rect2i" => "Rect2I", - - "mat2" => "Vector4", - "mat3" => "Basis", - "mat4" => "Projection", - - "transform_2d" => "Transform2D", - "transform" => "Transform3D", - - "sampler2D" => "Texture2D", - "sampler2DArray" => "Texture2DArray", - "sampler3D" => "Texture3D", - "samplerCube" => "Cubemap", - "samplerExternalOES" => "ExternalTexture", - - _ => type, - }; - } + if (v.EndsWith(")")) + { + var args = v.Split('(').Last().TrimEnd(')').Split(',').Select(SafeValue); + return $"new {csType}({string.Join(",", args)})"; + } - protected override string Str() - { - return string.Join("\n", ShaderGlobals()); + return null; + } - IEnumerable ShaderGlobals() - { - foreach (var (type, name, @default, rawType) in this.ShaderGlobals) - yield return $"Type: {type}, Name: {name}, Default: {@default}, RawType: {rawType}"; + static string SafeValue(string v) + => v.Contains('.') ? $"{v}f" : v; } } + + #endregion } diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs index 88064e7..d7749c3 100644 --- a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs @@ -4,31 +4,35 @@ namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions; internal static class ShaderGlobalsScraper { - private static readonly Regex BeginGlobalRegex = new(@"^""?(?.+?)""?={$"); - private static readonly Regex EndGlobalRegex = new("^}$"); - private static readonly Regex TypeRegex = new(@"^""type"": ""(?.+)"",?$"); - private static readonly Regex DefaultRegex = new(@"^""value"": (?.+?),?$"); + private const string NameRegexStr = @"^""?(?.+?)""?={$"; + private const string TypeRegexStr = @"^""type"": ""(?.+?)"",$"; + private const string DefaultRegexStr = @"^""value"": ""?(?.*?)""?$"; - public record ShaderGlobal(string Type, string Name, string Default); + private static readonly Regex NameRegex = new(NameRegexStr, RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private static readonly Regex TypeRegex = new(TypeRegexStr, RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private static readonly Regex DefaultRegex = new(DefaultRegexStr, RegexOptions.Compiled | RegexOptions.ExplicitCapture); - public static IEnumerable GetShaderGlobals(string csFile, string gdRoot) + public record ShaderGlobal(string Name, string Type, string Default); + + public static IEnumerable GetShaderGlobals(string gdRoot) { - var gdFile = GD.GetProjectFile(csFile, gdRoot); - Log.Debug($"Scraping {gdFile} [Compiling {csFile}]"); + var gdPrj = GD.PRJ(gdRoot); + Log.Debug($"Scraping {gdPrj}"); - return MatchShaderConstants(gdFile); + return MatchShaderGlobals(gdPrj); - static IEnumerable MatchShaderConstants(string gdFile) + static IEnumerable MatchShaderGlobals(string gdFile) { + var wip = false; var found = false; string name = null; string type = null; - string @default = null; - foreach (var line in File.ReadLines(gdFile).Where(line => line != string.Empty)) + string dflt = null; + foreach (var line in File.ReadLines(gdFile) + .Where(line => line is not "")) { Log.Debug($"Line: {line}"); - if (line is "[shader_globals]") { found = true; @@ -37,45 +41,84 @@ static IEnumerable MatchShaderConstants(string gdFile) if (found) { - if (name != null) + if (MatchShaderGlobal(line)) + yield return new(name, type, dflt.NullIfEmpty()); + else if (!wip && line.StartsWith("[")) + yield break; + } + } + + bool MatchShaderGlobal(string line) + { + if (name is null) { MatchName(); return false; } + if (type is null) { MatchType(); return false; } + if (dflt is null) { MatchDflt(); return true; } + MatchEnd(); return false; + + void MatchName() + { + if (NameRegex.Match(line) is { Success: true } match) + { + Log.Debug($" - Name {NameRegex.GetGroupsAsStr(match)}"); + name = match.Groups["Name"].Value; + if (wip) EXPECTED("new ShaderGlobal"); + if (type is not null) EXPECTED("name before type"); + if (dflt is not null) EXPECTED("name before default"); + wip = true; + return; + } + + EXPECTED(NameRegexStr); + } + + void MatchType() + { + if (TypeRegex.Match(line) is { Success: true } match) { - if (EndGlobalRegex.IsMatch(line)) - { - if (type is null) - { - Log.Warn($" - Ignoring shader global without type {name}"); - continue; - } - - yield return new(type, name, @default); - - name = null; - type = null; - @default = null; - } - else if (TypeRegex.Match(line) is { Success: true } typeMatch) - { - if (type != null) - Log.Warn($" - Duplicate type {type} for shader global {name}"); - - type = typeMatch.Groups["Type"].Value; - } - else if (DefaultRegex.Match(line) is { Success: true } defaultMatch) - { - if (@default != null) - Log.Warn($" - Duplicate default {@defaultMatch} for shader global {name}"); - - @default = defaultMatch.Groups["Default"].Value; - } + Log.Debug($" - Type {TypeRegex.GetGroupsAsStr(match)}"); + type = match.Groups["Type"].Value; + if (!wip) EXPECTED("within ShaderGlobal"); + if (name is null) EXPECTED("name before type"); + if (dflt is not null) EXPECTED("type before default"); + return; } - else + + EXPECTED(TypeRegexStr); + } + + void MatchDflt() + { + if (DefaultRegex.Match(line) is { Success: true } match) { - if (line.StartsWith("[")) - yield break; - else if (BeginGlobalRegex.Match(line) is { Success: true } beginMatch) - name = beginMatch.Groups["Name"].Value; + Log.Debug($" - Default {DefaultRegex.GetGroupsAsStr(match)}"); + dflt = match.Groups["Default"].Value; + if (!wip) EXPECTED("within ShaderGlobal"); + if (name is null) EXPECTED("name before default"); + if (type is null) EXPECTED("type before default"); + return; } + + EXPECTED(DefaultRegexStr); } + + void MatchEnd() + { + if (line is "}") + { + Log.Debug($" - END"); + if (!wip) EXPECTED("within ShaderGlobal"); + if (name is null) EXPECTED("name before end"); + if (type is null) EXPECTED("type before end"); + if (dflt is null) EXPECTED("default before end"); + wip = false; name = null; type = null; dflt = null; + return; + } + + EXPECTED("}"); + } + + void EXPECTED(string reason) + => throw new Exception($"Malformed ShaderGlobal [Expected: {reason}, Found: {line} - Name: {name}, Type: {type}, Default: {dflt}]"); } } } diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs index 5e3ef5d..820802a 100644 --- a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs @@ -11,16 +11,12 @@ internal class ShaderGlobalsSourceGenerator : SourceGeneratorForDeclaredTypeWith protected override (string GeneratedCode, DiagnosticDetail Error) GenerateCode(Compilation compilation, SyntaxNode node, INamedTypeSymbol symbol, AttributeData attribute, AnalyzerConfigOptions options) { - var data = ReconstructAttribute(); - var model = new ShaderGlobalsDataModel(symbol, data.ClassPath, options.TryGetGodotProjectDir()); + var model = new ShaderGlobalsDataModel(symbol, GD.ROOT(node, options)); Log.Debug($"--- MODEL ---\n{model}\n"); - var output = ShaderGlobalsTemplate.Render(model, member => member.Name); + var output = ShaderGlobalsTemplate.Render(model, Shared.Utils); Log.Debug($"--- OUTPUT ---\n{output}\n"); return (output, null); - - Godot.ShaderGlobalsAttribute ReconstructAttribute() => new( - (string)attribute.ConstructorArguments[0].Value); } } diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban index abc02e7..4f4f6cb 100644 --- a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban +++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban @@ -1,25 +1,38 @@ -using System; -using System.ComponentModel; +{{-#####################-}} +{{-#####- CONTENT -#####-}} +{{-#####################-}} -using Godot; +{{~ capture body ~}} + public static class Default + { +{{~ for x in ShaderGlobals ~}} + public static readonly {{x.Type}} {{x.Name}} = {{x.Default}}; +{{~ end ~}} + } -{{~ Namespace ~}} + private static class Name + { +{{~ for x in ShaderGlobals ~}} + public static readonly StringName {{x.Name}} = "{{x.RawName}}"; +{{~ end ~}} + } -partial class {{ClassName}} -{ -{{~ for global in ShaderGlobals ~}} - private static readonly StringName _{{global.Name}}Name = "{{global.RawName}}"; - private static {{global.Type}} _{{global.Name}} = {{global.Default}}; - /// A statically typed wrapper for the shader global {{global.RawName}} defined in godot.project. - /// When the shader global is modified outside of this property, the change will not be reflected in the property. - public static {{global.Type}} {{global.Name}} + private static class Value { - get => _{{global.Name}}; - set - { - RenderingServer.GlobalShaderParameterSet(_{{global.Name}}Name, value); - _{{global.Name}} = value; - } +{{~ for x in ShaderGlobals ~}} + public static {{x.Type}} {{x.Name}} = Default.{{x.Name}}; +{{~ end ~}} } +{{~ for x in ShaderGlobals ~}} + + /// A statically typed wrapper for the shader global {{x.RawName}} defined in godot.project. + /// If the shader global is modified outside of this property, the change will not be reflected in the property. + public static {{x.Type}} {{x.Name}} { get => Value.{{x.Name}}; set => RenderingServer.GlobalShaderParameterSet(Name.{{x.Name}}, Value.{{x.Name}} = value); } +{{~ end ~}} {{~ end ~}} -} + +{{-##################-}} +{{-#####- MAIN -#####-}} +{{-##################-}} + +{{~ RenderClass body ~}} diff --git a/SourceGenerators/Utilities/Extensions/GodotExtensions.cs b/SourceGenerators/Utilities/Extensions/GodotExtensions.cs index ab74ed9..6f1161f 100644 --- a/SourceGenerators/Utilities/Extensions/GodotExtensions.cs +++ b/SourceGenerators/Utilities/Extensions/GodotExtensions.cs @@ -96,8 +96,15 @@ public static string ROOT(AnalyzerConfigOptions options, string csPath) public static string ROOT(SyntaxNode node, AnalyzerConfigOptions options) => options.TryGetGodotProjectDir() ?? GetProjectRoot(node.SyntaxTree.FilePath); - public static string RES(string path, string root) - => $"res://{path[root.Length..].Replace("\\", "/").TrimStart('/')}"; + public static string PRJ(string gdRoot) + { + var gdPrj = Path.Combine(gdRoot, GodotProjectFile); + return File.Exists(gdPrj) ? gdPrj : + throw new Exception($"Could not find {GodotProjectFile} in {gdRoot}"); + } + + public static string RES(string csPath, string gdRoot) + => $"res://{csPath[gdRoot.Length..].Replace("\\", "/").TrimStart('/')}"; public static string TSCN(SyntaxNode node, AnalyzerConfigOptions options) => Res("tscn", node, options); public static string TRES(SyntaxNode node, AnalyzerConfigOptions options) => Res("tres", node, options); diff --git a/SourceGenerators/Utilities/Extensions/StringExtensions.cs b/SourceGenerators/Utilities/Extensions/StringExtensions.cs index a3f19dd..5c40bef 100644 --- a/SourceGenerators/Utilities/Extensions/StringExtensions.cs +++ b/SourceGenerators/Utilities/Extensions/StringExtensions.cs @@ -42,18 +42,18 @@ static char LowercaseFirstWord(string x, int i) => i is 0 ? LowercaseFirstChar(x) : UppercaseFirstChar(x); } - public static string TrimPrefix(this string source, string prefix) - => source.StartsWith(prefix, StringComparison.Ordinal) ? source[prefix.Length..] : source; - - public static string TrimSuffix(this string source, string suffix) - => source.EndsWith(suffix, StringComparison.Ordinal) ? source[..^suffix.Length] : source; - public static string AddPrefix(this string source, string prefix) => source.StartsWith(prefix, StringComparison.Ordinal) ? source : $"{prefix}{source}"; public static string AddSuffix(this string source, string suffix) => source.EndsWith(suffix, StringComparison.Ordinal) ? source : $"{source}{suffix}"; + public static string TrimPrefix(this string source, string prefix) + => source.StartsWith(prefix, StringComparison.Ordinal) ? source[prefix.Length..] : source; + + public static string TrimSuffix(this string source, string suffix) + => source.EndsWith(suffix, StringComparison.Ordinal) ? source[..^suffix.Length] : source; + public static string Truncate(this string source, int maxChars) => source.Length <= maxChars ? source : source[..maxChars];