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/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/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/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs
new file mode 100644
index 0000000..28ea619
--- /dev/null
+++ b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs
@@ -0,0 +1,65 @@
+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/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/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid
new file mode 100644
index 0000000..5bd4640
--- /dev/null
+++ b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.cs.uid
@@ -0,0 +1 @@
+uid://dm8vgm0v4e6hi
diff --git a/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.tscn b/Godot 4 Tests/TestScenes/Feature164.ShaderGlobals/ShaderGlobalsAttributeTests.tscn
new file mode 100644
index 0000000..80f2b30
--- /dev/null
+++ b/Godot 4 Tests/TestScenes/Feature164.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/Feature164.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..23688f9 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/Feature164.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..779f21f
--- /dev/null
+++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsAttribute.cs
@@ -0,0 +1,4 @@
+namespace Godot;
+
+[AttributeUsage(AttributeTargets.Class)]
+public sealed class ShaderGlobalsAttribute() : Attribute;
diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs
new file mode 100644
index 0000000..f9398e9
--- /dev/null
+++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsDataModel.cs
@@ -0,0 +1,88 @@
+using Microsoft.CodeAnalysis;
+
+namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions;
+
+internal class ShaderGlobalsDataModel(INamedTypeSymbol symbol, string gdRoot) : ClassDataModel(symbol)
+{
+ public record ShaderGlobal(string Name, string Type, string Default, string RawName);
+
+ public IList ShaderGlobals { get; } = [..
+ ShaderGlobalsScraper
+ .GetShaderGlobals(gdRoot)
+ .Select(Convert)];
+
+ protected override string Str()
+ => string.Join("\n", ShaderGlobals);
+
+ #region Convert
+
+ private static ShaderGlobal Convert(ShaderGlobalsScraper.ShaderGlobal raw)
+ {
+ 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
+ {
+ "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,
+ };
+
+ string ConvertValue(string v)
+ {
+ return v is null ? null : TryAsRes() ?? TryAsCtor() ?? SafeValue(v);
+
+ string TryAsRes()
+ => v.StartsWith("res://") ? @$"GD.Load<{csType}>(""{v}"")" : null;
+
+ string TryAsCtor()
+ {
+ if (v.EndsWith(")"))
+ {
+ var args = v.Split('(').Last().TrimEnd(')').Split(',').Select(SafeValue);
+ return $"new {csType}({string.Join(",", args)})";
+ }
+
+ return null;
+ }
+
+ static string SafeValue(string v)
+ => v.Contains('.') ? $"{v}f" : v;
+ }
+ }
+
+ #endregion
+}
diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs
new file mode 100644
index 0000000..d7749c3
--- /dev/null
+++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsScraper.cs
@@ -0,0 +1,125 @@
+using System.Text.RegularExpressions;
+
+namespace GodotSharp.SourceGenerators.ShaderGlobalsExtensions;
+
+internal static class ShaderGlobalsScraper
+{
+ private const string NameRegexStr = @"^""?(?.+?)""?={$";
+ private const string TypeRegexStr = @"^""type"": ""(?.+?)"",$";
+ private const string DefaultRegexStr = @"^""value"": ""?(?.*?)""?$";
+
+ 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 record ShaderGlobal(string Name, string Type, string Default);
+
+ public static IEnumerable GetShaderGlobals(string gdRoot)
+ {
+ var gdPrj = GD.PRJ(gdRoot);
+ Log.Debug($"Scraping {gdPrj}");
+
+ return MatchShaderGlobals(gdPrj);
+
+ static IEnumerable MatchShaderGlobals(string gdFile)
+ {
+ var wip = false;
+ var found = false;
+ string name = null;
+ string type = null;
+ 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;
+ continue;
+ }
+
+ if (found)
+ {
+ 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)
+ {
+ 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;
+ }
+
+ EXPECTED(TypeRegexStr);
+ }
+
+ void MatchDflt()
+ {
+ if (DefaultRegex.Match(line) is { Success: true } match)
+ {
+ 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
new file mode 100644
index 0000000..820802a
--- /dev/null
+++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsSourceGenerator.cs
@@ -0,0 +1,22 @@
+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 model = new ShaderGlobalsDataModel(symbol, GD.ROOT(node, options));
+ Log.Debug($"--- MODEL ---\n{model}\n");
+
+ var output = ShaderGlobalsTemplate.Render(model, Shared.Utils);
+ Log.Debug($"--- OUTPUT ---\n{output}\n");
+
+ return (output, null);
+ }
+}
diff --git a/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban
new file mode 100644
index 0000000..4f4f6cb
--- /dev/null
+++ b/SourceGenerators/ShaderGlobalsExtensions/ShaderGlobalsTemplate.scriban
@@ -0,0 +1,38 @@
+{{-#####################-}}
+{{-#####- CONTENT -#####-}}
+{{-#####################-}}
+
+{{~ capture body ~}}
+ public static class Default
+ {
+{{~ for x in ShaderGlobals ~}}
+ public static readonly {{x.Type}} {{x.Name}} = {{x.Default}};
+{{~ end ~}}
+ }
+
+ private static class Name
+ {
+{{~ for x in ShaderGlobals ~}}
+ public static readonly StringName {{x.Name}} = "{{x.RawName}}";
+{{~ end ~}}
+ }
+
+ private static class 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];