Skip to content
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ All AI agents and contributors must follow these rules when writing, reviewing,
## Git Workflow Rules

- Every completed task MUST pass at least one build validation before it is considered done.
- When the goal is to inspect or reduce warnings printed during project build, contributors MUST establish the warning
baseline from a non-incremental repository-root build by running `dotnet clean` and then `dotnet build`.
- Contributors MUST NOT treat a repeated incremental `dotnet build` result as authoritative for warning inspection when
a clean baseline has not been captured in the same round.
- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project
`dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles.
- When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected
Expand Down Expand Up @@ -233,6 +237,10 @@ All generated or modified code MUST include clear and meaningful comments where
Use the smallest command set that proves the change, then expand if the change is cross-cutting.

```bash
# Check warnings from the default repository build entrypoint
dotnet clean
dotnet build

# Build the full solution
dotnet build GFramework.sln -c Release

Expand Down
1 change: 0 additions & 1 deletion GFramework.Core.Tests/GFramework.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0"/>
Expand Down
10 changes: 8 additions & 2 deletions GFramework.Game/Data/UnifiedSettingsDataRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,15 +283,21 @@ private async Task WriteUnifiedFileCoreAsync(UnifiedSettingsFile currentFile, Un
/// 复制当前统一文件快照,确保未提交修改不会污染内存中的已提交状态。
/// </summary>
/// <param name="source">要复制的统一文件快照。</param>
/// <returns>包含独立 section 字典的新快照。</returns>
/// <returns>包含独立 section 映射副本的新快照。</returns>
private static UnifiedSettingsFile CloneFile(UnifiedSettingsFile source)
{
ArgumentNullException.ThrowIfNull(source);

// 反序列化后的运行时类型可能只是 IDictionary 实现;若底层仍是 Dictionary,则保留其 comparer。
// 若 comparer 已因接口抽象而不可恢复,则显式回退到 Ordinal,避免让默认 comparer 语义继续隐式存在。
var sections = source.Sections is Dictionary<string, string> dictionary
? new Dictionary<string, string>(dictionary, dictionary.Comparer)
: new Dictionary<string, string>(source.Sections, StringComparer.Ordinal);

return new UnifiedSettingsFile
{
Version = source.Version,
Sections = new Dictionary<string, string>(source.Sections, source.Sections.Comparer)
Sections = sections
};
}

Expand Down
16 changes: 12 additions & 4 deletions GFramework.Game/Data/UnifiedSettingsFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using GFramework.Core.Abstractions.Versioning;

namespace GFramework.Game.Data;
Expand All @@ -22,13 +24,19 @@ namespace GFramework.Game.Data;
internal sealed class UnifiedSettingsFile : IVersioned
{
/// <summary>
/// 配置节集合,存储不同类型的配置数据
/// 键为配置节名称,值为配置对象
/// 配置节映射,存储不同类型的配置数据。
/// </summary>
public Dictionary<string, string> Sections { get; set; } = new();
/// <remarks>
/// 这里公开为 <see cref="IDictionary{TKey,TValue}" /> 而不是具体的 <see cref="Dictionary{TKey,TValue}" />,
/// 以避免暴露可替换的具体集合实现,同时继续兼容 Newtonsoft.Json 对字典对象的序列化与反序列化。
/// 默认实例使用 <see cref="StringComparer.Ordinal" />;若调用方提供其他实现,仓库在可以识别底层
/// <see cref="Dictionary{TKey,TValue}" /> comparer 时会保留原语义,否则克隆快照时会显式回退到
/// <see cref="StringComparer.Ordinal" />。
/// </remarks>
public IDictionary<string, string> Sections { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);

/// <summary>
/// 配置文件版本号,用于版本控制和兼容性检查
/// </summary>
public int Version { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,61 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture]
public class AutoSceneGeneratorTests
{
private const string AutoSceneAttributeWithKeyDeclaration = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute(string key) { }
}
""";

private const string AutoSceneAttributeWithoutKeyDeclaration = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute() { }
}
""";

private const string NodeTypes = """
public class Node { }
public class Node2D : Node { }
""";

private const string SceneBehaviorInfrastructure = """
namespace GFramework.Game.Abstractions.Scene
{
public interface ISceneBehavior { }
}

namespace GFramework.Godot.Scene
{
using GFramework.Game.Abstractions.Scene;
using Godot;

public static class SceneBehaviorFactory
{
public static ISceneBehavior Create<T>(T owner, string key)
where T : Node
{
return null!;
}
}
}
""";

[Test]
public async Task Generates_Scene_Behavior_Boilerplate()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;

namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute(string key) { }
}
}

namespace Godot
{
public class Node { }
public class Node2D : Node { }
}

namespace GFramework.Game.Abstractions.Scene
{
public interface ISceneBehavior { }
}

namespace GFramework.Godot.Scene
{
using GFramework.Game.Abstractions.Scene;
using Godot;

public static class SceneBehaviorFactory
{
public static ISceneBehavior Create<T>(T owner, string key)
where T : Node
{
return null!;
}
}
}

namespace TestApp
{
[AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D
{
}
}
""";
string source = CreateAutoSceneSource(
AutoSceneAttributeWithKeyDeclaration,
"""
[AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D
{
}
""",
includeBehaviorInfrastructure: true);

const string expected = """
// <auto-generated />
Expand All @@ -80,40 +84,20 @@ partial class GameplayRoot

await GeneratorTest<AutoSceneGenerator>.RunAsync(
source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
}

[Test]
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;

namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute() { }
}
}

namespace Godot
{
public class Node { }
public class Node2D : Node { }
}

namespace TestApp
{
[{|#0:AutoScene|}]
public partial class GameplayRoot : Node2D
{
}
}
""";
string source = CreateAutoSceneSource(
AutoSceneAttributeWithoutKeyDeclaration,
"""
[{|#0:AutoScene|}]
public partial class GameplayRoot : Node2D
{
}
""");

var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
{
Expand All @@ -128,65 +112,26 @@ public partial class GameplayRoot : Node2D
.WithLocation(0)
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));

await test.RunAsync();
await test.RunAsync().ConfigureAwait(false);
}

[Test]
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
{
const string source = """
#nullable enable
using System;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;

namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoSceneAttribute : Attribute
{
public AutoSceneAttribute(string key) { }
}
}

namespace Godot
{
public class Node { }
public class Node2D : Node { }
}

namespace GFramework.Game.Abstractions.Scene
{
public interface ISceneBehavior { }
}

namespace GFramework.Godot.Scene
{
using GFramework.Game.Abstractions.Scene;
using Godot;

public static class SceneBehaviorFactory
{
public static ISceneBehavior Create<T>(T owner, string key)
where T : Node
{
return null!;
}
}
}

namespace TestApp
{
[AutoScene("Gameplay")]
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
}
}
""";
string source = CreateAutoSceneSource(
AutoSceneAttributeWithKeyDeclaration,
"""
[AutoScene("Gameplay")]
public partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged> : Node2D
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
}
""",
includeBehaviorInfrastructure: true,
nullableEnabled: true);

const string expected = """
// <auto-generated />
Expand Down Expand Up @@ -214,7 +159,7 @@ partial class GameplayRoot<TReference, TNotNull, TValue, TUnmanaged>

await GeneratorTest<AutoSceneGenerator>.RunAsync(
source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
}

/// <summary>
Expand Down Expand Up @@ -267,7 +212,7 @@ public partial class GameplayRoot : Node2D
.WithLocation(0)
.WithArguments("GameplayRoot", "SceneKeyStr"));

await test.RunAsync();
await test.RunAsync().ConfigureAwait(false);
}

/// <summary>
Expand Down Expand Up @@ -326,6 +271,39 @@ public partial class GameplayRoot : Node2D
.WithLocation(0)
.WithArguments("GameplayRoot", "__autoSceneBehavior_Generated"));

await test.RunAsync();
await test.RunAsync().ConfigureAwait(false);
}

private static string CreateAutoSceneSource(
string attributeDeclaration,
string testAppSource,
bool includeBehaviorInfrastructure = false,
bool nullableEnabled = false)
{
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
string infrastructure = includeBehaviorInfrastructure
? $"{Environment.NewLine}{Environment.NewLine}{SceneBehaviorInfrastructure}"
: string.Empty;

return $$"""
{{nullableDirective}}using System;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;

namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
{{attributeDeclaration}}
}

namespace Godot
{
{{NodeTypes}}
}{{infrastructure}}

namespace TestApp
{
{{testAppSource}}
}
""";
}
}
Loading
Loading