Compare commits

..

No commits in common. "608e639c0fabaae92e33a5989c86e6d9e3202e7d" and "a8447a68a4fa36ea53517f5cf515af67b6fb74ea" have entirely different histories.

87 changed files with 2662 additions and 2805 deletions

View File

@ -29,10 +29,6 @@ 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
@ -237,10 +233,6 @@ 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

View File

@ -49,7 +49,7 @@ XML 注释。这里记录的是类型族级基线,成员级契约细节仍需
| `Configuration/` `Environment/` `Data/` `Serializer/` `Storage/` `Versioning/` | `7/7` 个类型声明已带 XML 注释 | `IConfigurationManager``IEnvironment``ILoadableFrom<T>``ISerializer``IStorage` |
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` `Enums/` `Properties/` | `19/19` 个类型声明已带 XML 注释 | `IPrioritized``IController``IModel``ISystem``IContextUtility``ArchitecturePhase` |
完整接入说明与阅读顺序见 [Core 抽象层说明](../docs/zh-CN/abstractions/core-abstractions.md)
完整 inventory 与阅读顺序见 `docs/zh-CN/abstractions/core-abstractions.md`
## 采用建议

View File

@ -7,6 +7,7 @@
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0"/>

View File

@ -75,7 +75,7 @@ XML 注释。这里先保留阅读基线,成员级 ``<param>`` / ``<returns>``
| `Logging/` `Localization/` `Configuration/` `Environment/` `Ioc/` | `31/31` 个类型声明已带 XML 注释 | `ConsoleLogger``LocalizationManager``ConfigurationManager``DefaultEnvironment``MicrosoftDiContainer` |
| `Model/` `Systems/` `Utility/` `Rule/` `Extensions/` `Functional/` | `34/34` 个类型声明已带 XML 注释 | `AbstractModel``AbstractSystem``NumericDisplayFormatter``ContextAwareBase``Result<T>` |
完整的模块化接入说明和阅读顺序见 [Core 栏目](../docs/zh-CN/core/index.md)
完整的模块化阅读顺序和 inventory 说明见 `docs/zh-CN/core/index.md`
## 最小接入路径

View File

@ -97,5 +97,5 @@ public sealed class GetPlayerProfileHandler
## 文档入口
- 运行时与整体接入说明:[CQRS 栏目](../docs/zh-CN/core/cqrs.md)
- 如果你需要默认实现而不是契约层,请看 [GFramework.Cqrs README](../GFramework.Cqrs/README.md)
- 运行时与整体接入说明:`docs/zh-CN/core/cqrs.md`
- 如果你需要默认实现而不是契约层,请看 `GFramework.Cqrs/README.md`

View File

@ -150,5 +150,5 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
## 文档入口
- 总体文档:[CQRS 栏目](../docs/zh-CN/core/cqrs.md)
- 契约层说明:[GFramework.Cqrs.Abstractions README](../GFramework.Cqrs.Abstractions/README.md)
- 总体文档:`docs/zh-CN/core/cqrs.md`
- 契约层说明:`GFramework.Cqrs.Abstractions/README.md`

View File

@ -236,14 +236,14 @@ public sealed class ContinueGameCommandHandler
虽然大部分详细文档写在 `GFramework.Game` 侧,但阅读顺序仍然适用于本包:
- 游戏模块总览:[Game 模块总览](../docs/zh-CN/game/index.md)
- 内容配置系统:[内容配置系统](../docs/zh-CN/game/config-system.md)
- 数据与存档:[数据与存档系统](../docs/zh-CN/game/data.md)
- 设置系统:[设置系统](../docs/zh-CN/game/setting.md)
- 存储系统:[存储系统](../docs/zh-CN/game/storage.md)
- 序列化系统:[序列化系统](../docs/zh-CN/game/serialization.md)
- 场景系统:[场景系统](../docs/zh-CN/game/scene.md)
- UI 系统:[UI 系统](../docs/zh-CN/game/ui.md)
- 游戏模块总览:[docs/zh-CN/game/index.md](../docs/zh-CN/game/index.md)
- 内容配置系统:[docs/zh-CN/game/config-system.md](../docs/zh-CN/game/config-system.md)
- 数据与存档:[docs/zh-CN/game/data.md](../docs/zh-CN/game/data.md)
- 设置系统:[docs/zh-CN/game/setting.md](../docs/zh-CN/game/setting.md)
- 存储系统:[docs/zh-CN/game/storage.md](../docs/zh-CN/game/storage.md)
- 序列化系统:[docs/zh-CN/game/serialization.md](../docs/zh-CN/game/serialization.md)
- 场景系统:[docs/zh-CN/game/scene.md](../docs/zh-CN/game/scene.md)
- UI 系统:[docs/zh-CN/game/ui.md](../docs/zh-CN/game/ui.md)
## 选择建议

View File

@ -283,21 +283,15 @@ public class UnifiedSettingsDataRepository(
/// 复制当前统一文件快照,确保未提交修改不会污染内存中的已提交状态。
/// </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 = sections
Sections = new Dictionary<string, string>(source.Sections, source.Sections.Comparer)
};
}

View File

@ -11,8 +11,6 @@
// 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;
@ -24,19 +22,13 @@ namespace GFramework.Game.Data;
internal sealed class UnifiedSettingsFile : IVersioned
{
/// <summary>
/// 配置节映射,存储不同类型的配置数据。
/// 配置节集合,存储不同类型的配置数据
/// 键为配置节名称,值为配置对象
/// </summary>
/// <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);
public Dictionary<string, string> Sections { get; set; } = new();
/// <summary>
/// 配置文件版本号,用于版本控制和兼容性检查
/// </summary>
public int Version { get; set; }
}
}

View File

@ -57,8 +57,8 @@
对应文档:
- [内容配置系统](../docs/zh-CN/game/config-system.md)
- [Game 模块总览](../docs/zh-CN/game/index.md)
- [docs/zh-CN/game/config-system.md](../docs/zh-CN/game/config-system.md)
- [docs/zh-CN/game/index.md](../docs/zh-CN/game/index.md)
### `Data/`
@ -81,8 +81,8 @@
对应文档:
- [数据与存档系统](../docs/zh-CN/game/data.md)
- [设置系统](../docs/zh-CN/game/setting.md)
- [docs/zh-CN/game/data.md](../docs/zh-CN/game/data.md)
- [docs/zh-CN/game/setting.md](../docs/zh-CN/game/setting.md)
### `Setting/`
@ -105,7 +105,7 @@
对应文档:
- [设置系统](../docs/zh-CN/game/setting.md)
- [docs/zh-CN/game/setting.md](../docs/zh-CN/game/setting.md)
### `Storage/`
@ -121,7 +121,7 @@
对应文档:
- [存储系统](../docs/zh-CN/game/storage.md)
- [docs/zh-CN/game/storage.md](../docs/zh-CN/game/storage.md)
- [GFramework.Game/Storage/ReadMe.md](./Storage/ReadMe.md)
### `Serializer/`
@ -134,7 +134,7 @@
对应文档:
- [序列化系统](../docs/zh-CN/game/serialization.md)
- [docs/zh-CN/game/serialization.md](../docs/zh-CN/game/serialization.md)
### `Scene/``UI/`
@ -157,8 +157,8 @@
对应文档:
- [场景系统](../docs/zh-CN/game/scene.md)
- [UI 系统](../docs/zh-CN/game/ui.md)
- [docs/zh-CN/game/scene.md](../docs/zh-CN/game/scene.md)
- [docs/zh-CN/game/ui.md](../docs/zh-CN/game/ui.md)
### `Routing/``State/`
@ -293,7 +293,7 @@ var registry = bootstrap.Registry;
这一能力几乎总是与 source generators 绑定使用。目录、schema、生成器与热重载约定请直接看
- [内容配置系统](../docs/zh-CN/game/config-system.md)
- [docs/zh-CN/game/config-system.md](../docs/zh-CN/game/config-system.md)
### 4. 接入 Scene / UI 路由
@ -342,14 +342,14 @@ public sealed class MyUiRouter : UiRouterBase
## 文档入口
- 游戏模块总览:[Game 模块总览](../docs/zh-CN/game/index.md)
- 内容配置系统:[内容配置系统](../docs/zh-CN/game/config-system.md)
- 数据与存档:[数据与存档系统](../docs/zh-CN/game/data.md)
- 设置系统:[设置系统](../docs/zh-CN/game/setting.md)
- 存储系统:[存储系统](../docs/zh-CN/game/storage.md)
- 序列化系统:[序列化系统](../docs/zh-CN/game/serialization.md)
- 场景系统:[场景系统](../docs/zh-CN/game/scene.md)
- UI 系统:[UI 系统](../docs/zh-CN/game/ui.md)
- 游戏模块总览:[docs/zh-CN/game/index.md](../docs/zh-CN/game/index.md)
- 内容配置系统:[docs/zh-CN/game/config-system.md](../docs/zh-CN/game/config-system.md)
- 数据与存档:[docs/zh-CN/game/data.md](../docs/zh-CN/game/data.md)
- 设置系统:[docs/zh-CN/game/setting.md](../docs/zh-CN/game/setting.md)
- 存储系统:[docs/zh-CN/game/storage.md](../docs/zh-CN/game/storage.md)
- 序列化系统:[docs/zh-CN/game/serialization.md](../docs/zh-CN/game/serialization.md)
- 场景系统:[docs/zh-CN/game/scene.md](../docs/zh-CN/game/scene.md)
- UI 系统:[docs/zh-CN/game/ui.md](../docs/zh-CN/game/ui.md)
## 什么时候不该直接依赖本包

View File

@ -6,61 +6,57 @@ 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()
{
string source = CreateAutoSceneSource(
AutoSceneAttributeWithKeyDeclaration,
"""
[AutoScene("Gameplay")]
public partial class GameplayRoot : Node2D
{
}
""",
includeBehaviorInfrastructure: true);
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
{
}
}
""";
const string expected = """
// <auto-generated />
@ -84,20 +80,40 @@ public class AutoSceneGeneratorTests
await GeneratorTest<AutoSceneGenerator>.RunAsync(
source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
}
[Test]
public async Task Reports_Diagnostic_When_AutoScene_Arguments_Are_Invalid()
{
string source = CreateAutoSceneSource(
AutoSceneAttributeWithoutKeyDeclaration,
"""
[{|#0:AutoScene|}]
public partial class GameplayRoot : Node2D
{
}
""");
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
{
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoSceneGenerator, DefaultVerifier>
{
@ -112,26 +128,65 @@ public class AutoSceneGeneratorTests
.WithLocation(0)
.WithArguments("AutoSceneAttribute", "GameplayRoot", "a single string scene key argument"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
public async Task Generates_Type_Constraints_For_Nullable_Reference_NotNull_And_Unmanaged_Parameters()
{
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 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
{
}
}
""";
const string expected = """
// <auto-generated />
@ -159,7 +214,7 @@ public class AutoSceneGeneratorTests
await GeneratorTest<AutoSceneGenerator>.RunAsync(
source,
("TestApp_GameplayRoot.AutoScene.g.cs", expected)).ConfigureAwait(false);
("TestApp_GameplayRoot.AutoScene.g.cs", expected));
}
/// <summary>
@ -212,7 +267,7 @@ public class AutoSceneGeneratorTests
.WithLocation(0)
.WithArguments("GameplayRoot", "SceneKeyStr"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
/// <summary>
@ -271,39 +326,6 @@ public class AutoSceneGeneratorTests
.WithLocation(0)
.WithArguments("GameplayRoot", "__autoSceneBehavior_Generated"));
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}}
}
""";
await test.RunAsync();
}
}

View File

@ -6,85 +6,69 @@ namespace GFramework.Godot.SourceGenerators.Tests.Behavior;
[TestFixture]
public class AutoUiPageGeneratorTests
{
private const string AutoUiPageAttributeWithLayerDeclaration = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoUiPageAttribute : Attribute
{
public AutoUiPageAttribute(string key, string layerName) { }
}
""";
private const string AutoUiPageAttributeWithoutLayerDeclaration = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoUiPageAttribute : Attribute
{
public AutoUiPageAttribute(string key) { }
}
""";
private const string CanvasNodeTypes = """
public class Node { }
public class CanvasItem : Node { }
public class Control : CanvasItem { }
""";
private const string UiLayerFullEnum = """
namespace GFramework.Game.Abstractions.Enums
{
public enum UiLayer
{
Page,
Overlay,
Modal
}
}
""";
private const string UiLayerPageOnlyEnum = """
namespace GFramework.Game.Abstractions.Enums
{
public enum UiLayer
{
Page
}
}
""";
private const string UiBehaviorInfrastructure = """
namespace GFramework.Game.Abstractions.UI
{
public interface IUiPageBehavior { }
}
namespace GFramework.Godot.UI
{
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
using Godot;
public static class UiPageBehaviorFactory
{
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
""";
[Test]
public async Task Generates_Ui_Page_Behavior_Boilerplate()
{
string source = CreateAutoUiPageSource(
AutoUiPageAttributeWithLayerDeclaration,
UiLayerFullEnum,
"""
[AutoUiPage("MainMenu", "Page")]
public partial class MainMenu : Control
{
}
""");
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 AutoUiPageAttribute : Attribute
{
public AutoUiPageAttribute(string key, string layerName) { }
}
}
namespace Godot
{
public class Node { }
public class CanvasItem : Node { }
public class Control : CanvasItem { }
}
namespace GFramework.Game.Abstractions.Enums
{
public enum UiLayer
{
Page,
Overlay,
Modal
}
}
namespace GFramework.Game.Abstractions.UI
{
public interface IUiPageBehavior { }
}
namespace GFramework.Godot.UI
{
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
using Godot;
public static class UiPageBehaviorFactory
{
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
namespace TestApp
{
[AutoUiPage("MainMenu", "Page")]
public partial class MainMenu : Control
{
}
}
""";
const string expected = """
// <auto-generated />
@ -108,21 +92,70 @@ public class AutoUiPageGeneratorTests
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
source,
("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
}
[Test]
public async Task Reports_Diagnostic_When_AutoUiPage_Attribute_Arguments_Are_Invalid()
{
string source = CreateAutoUiPageSource(
AutoUiPageAttributeWithoutLayerDeclaration,
UiLayerPageOnlyEnum,
"""
[{|#0:AutoUiPage("MainMenu")|}]
public partial class MainMenu : Control
{
}
""");
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 AutoUiPageAttribute : Attribute
{
public AutoUiPageAttribute(string key) { }
}
}
namespace Godot
{
public class Node { }
public class CanvasItem : Node { }
public class Control : CanvasItem { }
}
namespace GFramework.Game.Abstractions.Enums
{
public enum UiLayer
{
Page
}
}
namespace GFramework.Game.Abstractions.UI
{
public interface IUiPageBehavior { }
}
namespace GFramework.Godot.UI
{
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
using Godot;
public static class UiPageBehaviorFactory
{
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
namespace TestApp
{
[{|#0:AutoUiPage("MainMenu")|}]
public partial class MainMenu : Control
{
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoUiPageGenerator, DefaultVerifier>
{
@ -141,25 +174,74 @@ public class AutoUiPageGeneratorTests
"MainMenu",
"a string key argument and a string UiLayer name argument"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
public async Task Generates_Type_Constraints_For_ClassNullable_NotNull_And_Unmanaged()
{
string source = CreateAutoUiPageSource(
AutoUiPageAttributeWithLayerDeclaration,
UiLayerPageOnlyEnum,
"""
[AutoUiPage("MainMenu", "Page")]
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
where TReference : class?
where TNotNull : notnull
where TUnmanaged : unmanaged
{
}
""",
nullableEnabled: true);
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 AutoUiPageAttribute : Attribute
{
public AutoUiPageAttribute(string key, string layerName) { }
}
}
namespace Godot
{
public class Node { }
public class CanvasItem : Node { }
public class Control : CanvasItem { }
}
namespace GFramework.Game.Abstractions.Enums
{
public enum UiLayer
{
Page
}
}
namespace GFramework.Game.Abstractions.UI
{
public interface IUiPageBehavior { }
}
namespace GFramework.Godot.UI
{
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
using Godot;
public static class UiPageBehaviorFactory
{
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
where T : CanvasItem
{
return null!;
}
}
}
namespace TestApp
{
[AutoUiPage("MainMenu", "Page")]
public partial class MainMenu<TReference, TNotNull, TUnmanaged> : Control
where TReference : class?
where TNotNull : notnull
where TUnmanaged : unmanaged
{
}
}
""";
const string expected = """
// <auto-generated />
@ -186,40 +268,6 @@ public class AutoUiPageGeneratorTests
await GeneratorTest<AutoUiPageGenerator>.RunAsync(
source,
("TestApp_MainMenu.AutoUiPage.g.cs", expected)).ConfigureAwait(false);
}
private static string CreateAutoUiPageSource(
string attributeDeclaration,
string uiLayerDeclaration,
string testAppSource,
bool nullableEnabled = false)
{
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
return $$"""
{{nullableDirective}}using System;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
{{attributeDeclaration}}
}
namespace Godot
{
{{CanvasNodeTypes}}
}
{{uiLayerDeclaration}}
{{UiBehaviorInfrastructure}}
namespace TestApp
{
{{testAppSource}}
}
""";
("TestApp_MainMenu.AutoUiPage.g.cs", expected));
}
}

View File

@ -8,103 +8,93 @@ namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal;
[TestFixture]
public class BindNodeSignalGeneratorTests
{
private const string BindNodeSignalAttributeDeclaration = """
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
""";
private const string GetNodeAttributeDeclaration = """
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
}
""";
private const string EmptyNodeType = """
public class Node
{
}
""";
private const string LifecycleNodeType = """
public class Node
{
public virtual void _Ready() {}
public virtual void _ExitTree() {}
}
""";
private const string ButtonType = """
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
""";
private const string SpinBoxType = """
public class SpinBox : Node
{
public delegate void ValueChangedEventHandler(double value);
public event ValueChangedEventHandler? ValueChanged
{
add {}
remove {}
}
}
""";
/// <summary>
/// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
/// </summary>
[Test]
public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!;
private SpinBox _startOreSpinBox = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
[BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
private void OnStartOreValueChanged(double value)
{
}
public string NodeFieldName { get; }
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public string SignalName { get; }
}
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
""",
LifecycleNodeType,
ButtonType,
SpinBoxType);
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public virtual void _ExitTree() {}
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
public class SpinBox : Node
{
public delegate void ValueChangedEventHandler(double value);
public event ValueChangedEventHandler? ValueChanged
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
private SpinBox _startOreSpinBox = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
[BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
private void OnStartOreValueChanged(double value)
{
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
}
""";
const string expected = """
// <auto-generated />
@ -131,7 +121,7 @@ public class BindNodeSignalGeneratorTests
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
source,
("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
("TestApp_Hud.BindNodeSignal.g.cs", expected));
}
/// <summary>
@ -140,23 +130,70 @@ public class BindNodeSignalGeneratorTests
[Test]
public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration, GetNodeAttributeDeclaration),
"""
[GetNode]
private Button _startButton = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[GetNode]
private Button _cancelButton = null!;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
private void OnAnyButtonPressed()
{
}
""",
LifecycleNodeType,
ButtonType);
public string NodeFieldName { get; }
public string SignalName { get; }
}
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public virtual void _ExitTree() {}
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
[GetNode]
private Button _startButton = null!;
[GetNode]
private Button _cancelButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
private void OnAnyButtonPressed()
{
}
}
}
""";
const string expected = """
// <auto-generated />
@ -183,7 +220,7 @@ public class BindNodeSignalGeneratorTests
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
source,
("TestApp_Hud.BindNodeSignal.g.cs", expected)).ConfigureAwait(false);
("TestApp_Hud.BindNodeSignal.g.cs", expected));
}
/// <summary>
@ -192,195 +229,58 @@ public class BindNodeSignalGeneratorTests
[Test]
public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
private void OnStartButtonPressed()
{
}
""",
EmptyNodeType,
ButtonType);
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
await VerifyDiagnosticsAsync(
source,
new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("_startButton", "Released")).ConfigureAwait(false);
}
public string NodeFieldName { get; }
/// <summary>
/// 验证方法签名与事件委托不匹配时会报告错误。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private SpinBox _startOreSpinBox = null!;
public string SignalName { get; }
}
}
[{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
private void OnStartOreValueChanged()
{
}
""",
EmptyNodeType,
SpinBoxType);
namespace Godot
{
public class Node
{
}
await VerifyDiagnosticsAsync(
source,
new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox")).ConfigureAwait(false);
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
/// <summary>
/// 验证特性构造参数为空时会报告明确的参数无效诊断。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!;
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[{|#0:BindNodeSignal(nameof(_startButton), "")|}]
private void OnStartButtonPressed()
{
}
""",
EmptyNodeType,
ButtonType);
[{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
private void OnStartButtonPressed()
{
}
}
}
""";
await VerifyDiagnosticsAsync(
source,
new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("OnStartButtonPressed", "signalName")).ConfigureAwait(false);
}
/// <summary>
/// 验证当用户自定义了与生成方法同名的成员时,会报告冲突而不是生成重复成员。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
private void {|#0:__BindNodeSignals_Generated|}()
{
}
private void {|#1:__UnbindNodeSignals_Generated|}()
{
}
""",
EmptyNodeType,
ButtonType);
await VerifyDiagnosticsAsync(
source,
new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("Hud", "__BindNodeSignals_Generated"),
new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
.WithLocation(1)
.WithArguments("Hud", "__UnbindNodeSignals_Generated")).ConfigureAwait(false);
}
/// <summary>
/// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。
/// </summary>
[Test]
public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods()
{
string source = CreateHudSource(
CreateAbstractionsSource(BindNodeSignalAttributeDeclaration),
"""
private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
public override void {|#0:_Ready|}()
{
}
public override void {|#1:_ExitTree|}()
{
}
""",
LifecycleNodeType,
ButtonType);
await VerifyDiagnosticsAsync(
source,
new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
.WithLocation(0)
.WithArguments("Hud"),
new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
.WithLocation(1)
.WithArguments("Hud")).ConfigureAwait(false);
}
private static string CreateAbstractionsSource(params string[] attributeDeclarations)
{
string declarations = string.Join($"{Environment.NewLine}{Environment.NewLine}", attributeDeclarations);
return $$"""
namespace GFramework.Godot.SourceGenerators.Abstractions
{
{{declarations}}
}
""";
}
private static string CreateHudSource(
string abstractionsSource,
string hudMembers,
params string[] godotTypes)
{
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", godotTypes);
return $$"""
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
{{abstractionsSource}}
namespace Godot
{
{{godotSource}}
}
namespace TestApp
{
public partial class Hud : Node
{
{{hudMembers}}
}
}
""";
}
private static Task VerifyDiagnosticsAsync(string source, params DiagnosticResult[] expectedDiagnostics)
{
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
@ -391,11 +291,339 @@ public class BindNodeSignalGeneratorTests
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
{
test.ExpectedDiagnostics.Add(expectedDiagnostic);
}
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("_startButton", "Released"));
return test.RunAsync();
await test.RunAsync();
}
}
/// <summary>
/// 验证方法签名与事件委托不匹配时会报告错误。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
}
public class SpinBox : Node
{
public delegate void ValueChangedEventHandler(double value);
public event ValueChangedEventHandler? ValueChanged
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private SpinBox _startOreSpinBox = null!;
[{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
private void OnStartOreValueChanged()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox"));
await test.RunAsync();
}
/// <summary>
/// 验证特性构造参数为空时会报告明确的参数无效诊断。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[{|#0:BindNodeSignal(nameof(_startButton), "")|}]
private void OnStartButtonPressed()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("OnStartButtonPressed", "signalName"));
await test.RunAsync();
}
/// <summary>
/// 验证当用户自定义了与生成方法同名的成员时,会报告冲突而不是生成重复成员。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
private void {|#0:__BindNodeSignals_Generated|}()
{
}
private void {|#1:__UnbindNodeSignals_Generated|}()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("Hud", "__BindNodeSignals_Generated"));
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
.WithLocation(1)
.WithArguments("Hud", "__UnbindNodeSignals_Generated"));
await test.RunAsync();
}
/// <summary>
/// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。
/// </summary>
[Test]
public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public virtual void _ExitTree() {}
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
public override void {|#0:_Ready|}()
{
}
public override void {|#1:_ExitTree|}()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
.WithLocation(0)
.WithArguments("Hud"));
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
.WithLocation(1)
.WithArguments("Hud"));
await test.RunAsync();
}
}

View File

@ -29,7 +29,7 @@ public static class GeneratorTest<TGenerator>
test.TestState.GeneratedSources.Add(
(typeof(TGenerator), filename, NormalizeLineEndings(content)));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
/// <summary>
@ -44,4 +44,4 @@ public static class GeneratorTest<TGenerator>
.Replace("\r", "\n", StringComparison.Ordinal)
.Replace("\n", Environment.NewLine, StringComparison.Ordinal);
}
}
}

View File

@ -5,88 +5,61 @@ namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
[TestFixture]
public class GetNodeGeneratorTests
{
private const string FullGetNodeAttributeDeclaration = """
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public GetNodeAttribute() {}
public GetNodeAttribute(string path) { Path = path; }
public string? Path { get; set; }
public bool Required { get; set; } = true;
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
}
public enum NodeLookupMode
{
Auto = 0,
UniqueName = 1,
RelativePath = 2,
AbsolutePath = 3
}
""";
private const string MinimalGetNodeAttributeDeclaration = """
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public GetNodeAttribute() {}
}
public enum NodeLookupMode
{
Auto = 0
}
""";
private const string PropertyOnlyGetNodeAttributeDeclaration = """
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public string? Path { get; set; }
public bool Required { get; set; } = true;
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
}
public enum NodeLookupMode
{
Auto = 0,
UniqueName = 1,
RelativePath = 2,
AbsolutePath = 3
}
""";
private const string NodeWithReadyAndLookupMethods = """
public class Node
{
public virtual void _Ready() {}
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(string path) where T : Node => default;
}
""";
private const string HBoxContainerType = """
public class HBoxContainer : Node
{
}
""";
[Test]
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
{
string source = CreateGetNodeSource(
FullGetNodeAttributeDeclaration,
"""
public partial class TopBar : HBoxContainer
{
[GetNode]
private HBoxContainer _leftContainer = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[GetNode]
private HBoxContainer m_rightContainer = null!;
}
""",
HBoxContainerType);
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public GetNodeAttribute() {}
public GetNodeAttribute(string path) { Path = path; }
public string? Path { get; set; }
public bool Required { get; set; } = true;
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
}
public enum NodeLookupMode
{
Auto = 0,
UniqueName = 1,
RelativePath = 2,
AbsolutePath = 3
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(string path) where T : Node => default;
}
public class HBoxContainer : Node
{
}
}
namespace TestApp
{
public partial class TopBar : HBoxContainer
{
[GetNode]
private HBoxContainer _leftContainer = null!;
[GetNode]
private HBoxContainer m_rightContainer = null!;
}
}
""";
const string expected = """
// <auto-generated />
@ -115,30 +88,69 @@ public class GetNodeGeneratorTests
await GeneratorTest<GetNodeGenerator>.RunAsync(
source,
("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
("TestApp_TopBar.GetNode.g.cs", expected));
}
[Test]
public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists()
{
string source = CreateGetNodeSource(
FullGetNodeAttributeDeclaration,
"""
public partial class TopBar : HBoxContainer
{
[GetNode("%LeftContainer")]
private HBoxContainer _leftContainer = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
private HBoxContainer? _rightContainer;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public GetNodeAttribute() {}
public GetNodeAttribute(string path) { Path = path; }
public string? Path { get; set; }
public bool Required { get; set; } = true;
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
}
public override void _Ready()
{
__InjectGetNodes_Generated();
}
}
""",
HBoxContainerType);
public enum NodeLookupMode
{
Auto = 0,
UniqueName = 1,
RelativePath = 2,
AbsolutePath = 3
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(string path) where T : Node => default;
}
public class HBoxContainer : Node
{
}
}
namespace TestApp
{
public partial class TopBar : HBoxContainer
{
[GetNode("%LeftContainer")]
private HBoxContainer _leftContainer = null!;
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
private HBoxContainer? _rightContainer;
public override void _Ready()
{
__InjectGetNodes_Generated();
}
}
}
""";
const string expected = """
// <auto-generated />
@ -159,7 +171,7 @@ public class GetNodeGeneratorTests
await GeneratorTest<GetNodeGenerator>.RunAsync(
source,
("TestApp_TopBar.GetNode.g.cs", expected)).ConfigureAwait(false);
("TestApp_TopBar.GetNode.g.cs", expected));
}
[Test]
@ -222,26 +234,58 @@ public class GetNodeGeneratorTests
.WithSpan(39, 24, 39, 38)
.WithArguments("_leftContainer"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
{
string source = CreateGetNodeSource(
MinimalGetNodeAttributeDeclaration,
"""
public partial class TopBar : HBoxContainer
{
[GetNode]
private HBoxContainer _leftContainer = null!;
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
private void {|#0:__InjectGetNodes_Generated|}()
{
}
}
""",
HBoxContainerType);
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
public GetNodeAttribute() {}
}
public enum NodeLookupMode
{
Auto = 0
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
public T? GetNodeOrNull<T>(string path) where T : Node => default;
}
public class HBoxContainer : Node
{
}
}
namespace TestApp
{
public partial class TopBar : HBoxContainer
{
[GetNode]
private HBoxContainer _leftContainer = null!;
private void {|#0:__InjectGetNodes_Generated|}()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier>
{
@ -257,39 +301,6 @@ public class GetNodeGeneratorTests
.WithLocation(0)
.WithArguments("TopBar", "__InjectGetNodes_Generated"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
private static string CreateGetNodeSource(
string attributeDeclaration,
string testAppSource,
params string[] godotTypes)
{
string[] allGodotTypes = new string[godotTypes.Length + 1];
allGodotTypes[0] = NodeWithReadyAndLookupMethods;
Array.Copy(godotTypes, 0, allGodotTypes, 1, godotTypes.Length);
string godotSource = string.Join($"{Environment.NewLine}{Environment.NewLine}", allGodotTypes);
return $$"""
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
{{attributeDeclaration}}
}
namespace Godot
{
{{godotSource}}
}
namespace TestApp
{
{{testAppSource}}
}
""";
}
}
}

View File

@ -8,131 +8,6 @@ namespace GFramework.Godot.SourceGenerators.Tests.Project;
[TestFixture]
public class GodotProjectMetadataGeneratorTests
{
private const string AutoLoadProjectFile = """
[autoload]
GameServices="*res://autoload/game_services.tscn"
AudioBus="*res://autoload/audio_bus.gd"
""";
private const string InputActionsProjectFile = """
[input]
move_up={
"deadzone": 0.5
}
ui_cancel={
"deadzone": 0.5
}
""";
private const string ExpectedAutoLoads = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
/// </summary>
public static partial class AutoLoads
{
/// <summary>
/// 获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");
/// <summary>
/// 尝试获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static bool TryGetGameServices(out global::TestApp.GameServices? value)
{
return TryGetNode("GameServices", out value);
}
/// <summary>
/// 获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");
/// <summary>
/// 尝试获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static bool TryGetAudioBus(out global::Godot.Node? value)
{
return TryGetNode("AudioBus", out value);
}
/// <summary>
/// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <returns>已解析的 AutoLoad 节点。</returns>
private static TNode GetRequiredNode<TNode>(string autoLoadName)
where TNode : global::Godot.Node
{
if (TryGetNode(autoLoadName, out TNode? value))
{
return value!;
}
throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
}
/// <summary>
/// 尝试从当前 SceneTree 根节点解析 AutoLoad。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <param name="value">解析到的节点实例。</param>
/// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad则返回 <c>true</c>。</returns>
private static bool TryGetNode<TNode>(string autoLoadName, out TNode? value)
where TNode : global::Godot.Node
{
value = default;
if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
{
return false;
}
var root = sceneTree.Root;
if (root is null)
{
return false;
}
value = root.GetNodeOrNull<TNode>($"/root/{autoLoadName}");
return value is not null;
}
}
""";
private const string ExpectedInputActions = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 Input Action 名称的强类型常量。
/// </summary>
public static partial class InputActions
{
/// <summary>
/// Input Action <c>move_up</c> 的稳定名称。
/// </summary>
public const string MoveUp = "move_up";
/// <summary>
/// Input Action <c>ui_cancel</c> 的稳定名称。
/// </summary>
public const string UiCancel = "ui_cancel";
}
""";
/// <summary>
/// 验证会根据 AutoLoad 与 Input Action 生成稳定的强类型入口。
/// </summary>
@ -154,19 +29,142 @@ public class GodotProjectMetadataGeneratorTests
""",
includeAutoLoadAttribute: true);
const string projectFile = """
[autoload]
GameServices="*res://autoload/game_services.tscn"
AudioBus="*res://autoload/audio_bus.gd"
[input]
move_up={
"deadzone": 0.5
}
ui_cancel={
"deadzone": 0.5
}
""";
const string expectedAutoLoads = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 AutoLoad 单例的强类型访问入口。
/// </summary>
public static partial class AutoLoads
{
/// <summary>
/// 获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static global::TestApp.GameServices GameServices => GetRequiredNode<global::TestApp.GameServices>("GameServices");
/// <summary>
/// 尝试获取 AutoLoad <c>GameServices</c>。
/// </summary>
public static bool TryGetGameServices(out global::TestApp.GameServices? value)
{
return TryGetNode("GameServices", out value);
}
/// <summary>
/// 获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static global::Godot.Node AudioBus => GetRequiredNode<global::Godot.Node>("AudioBus");
/// <summary>
/// 尝试获取 AutoLoad <c>AudioBus</c>。
/// </summary>
public static bool TryGetAudioBus(out global::Godot.Node? value)
{
return TryGetNode("AudioBus", out value);
}
/// <summary>
/// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <returns>已解析的 AutoLoad 节点。</returns>
private static TNode GetRequiredNode<TNode>(string autoLoadName)
where TNode : global::Godot.Node
{
if (TryGetNode(autoLoadName, out TNode? value))
{
return value!;
}
throw new global::System.InvalidOperationException($"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.");
}
/// <summary>
/// 尝试从当前 SceneTree 根节点解析 AutoLoad。
/// </summary>
/// <typeparam name="TNode">节点类型。</typeparam>
/// <param name="autoLoadName">AutoLoad 名称。</param>
/// <param name="value">解析到的节点实例。</param>
/// <returns>若当前进程存在 SceneTree 且根节点中能解析到该 AutoLoad则返回 <c>true</c>。</returns>
private static bool TryGetNode<TNode>(string autoLoadName, out TNode? value)
where TNode : global::Godot.Node
{
value = default;
if (global::Godot.Engine.GetMainLoop() is not global::Godot.SceneTree sceneTree)
{
return false;
}
var root = sceneTree.Root;
if (root is null)
{
return false;
}
value = root.GetNodeOrNull<TNode>($"/root/{autoLoadName}");
return value is not null;
}
}
""";
const string expectedInputActions = """
// <auto-generated />
#nullable enable
namespace GFramework.Godot.Generated;
/// <summary>
/// 提供 project.godot 中 Input Action 名称的强类型常量。
/// </summary>
public static partial class InputActions
{
/// <summary>
/// Input Action <c>move_up</c> 的稳定名称。
/// </summary>
public const string MoveUp = "move_up";
/// <summary>
/// Input Action <c>ui_cancel</c> 的稳定名称。
/// </summary>
public const string UiCancel = "ui_cancel";
}
""";
var result = AdditionalTextGeneratorTestDriver.Run<GodotProjectMetadataGenerator>(
source,
("project.godot", $"{AutoLoadProjectFile}\n\n{InputActionsProjectFile}"));
("project.godot", projectFile));
var generatedSources = AdditionalTextGeneratorTestDriver.ToGeneratedSourceMap(result);
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
Assert.That(
generatedSources["GFramework_Godot_Generated_AutoLoads.g.cs"],
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedAutoLoads)));
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedAutoLoads)));
Assert.That(
generatedSources["GFramework_Godot_Generated_InputActions.g.cs"],
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(ExpectedInputActions)));
Is.EqualTo(AdditionalTextGeneratorTestDriver.NormalizeLineEndings(expectedInputActions)));
}
/// <summary>

View File

@ -6,52 +6,48 @@ namespace GFramework.Godot.SourceGenerators.Tests.Registration;
[TestFixture]
public class AutoRegisterExportedCollectionsGeneratorTests
{
private const string StandardAttributeDeclarations = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
""";
private const string MultiDeclarationAttributeDeclarations = """
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
""";
[Test]
public async Task Generates_Batch_Registration_Method_For_Annotated_Collections()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
[AutoRegisterExportedCollections]
public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
private readonly IntRegistry? _registry = new();
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true);
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry
{
public void Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper<TReference, TNotNull, TValue, TUnmanaged>
where TReference : class?
where TNotNull : notnull
where TValue : struct
where TUnmanaged : unmanaged
{
private readonly IntRegistry? _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
}
""";
const string expected = """
// <auto-generated />
@ -81,7 +77,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
[Test]
@ -135,29 +131,47 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0)
.WithArguments("Values"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Uses_Array_Parameter()
{
string source = CreateSource(
"""
public sealed class ArrayRegistry
{
public void Register(int[] value) { }
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly ArrayRegistry _registry = new();
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
public List<int[]> Values { get; } = new();
}
""",
nullableEnabled: true);
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class ArrayRegistry
{
public void Register(int[] value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly ArrayRegistry _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(ArrayRegistry.Register))]
public List<int[]> Values { get; } = new();
}
}
""";
const string expected = """
// <auto-generated />
@ -183,41 +197,59 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
{
string source = CreateSource(
"""
public interface IKeyValue<TKey, TValue>
{
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
public interface IRegistry<TKey, TValue>
{
void Registry(IKeyValue<TKey, TValue> mapping);
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
{
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
public sealed class IntConfig : IKeyValue<string, int>
{
}
namespace TestApp
{
public interface IKeyValue<TKey, TValue>
{
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IAssetRegistry<int>? _registry = null;
public interface IRegistry<TKey, TValue>
{
void Registry(IKeyValue<TKey, TValue> mapping);
}
[RegisterExportedCollection(nameof(_registry), "Registry")]
public List<IntConfig>? Values { get; } = new();
}
""",
nullableEnabled: true);
public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
{
}
public sealed class IntConfig : IKeyValue<string, int>
{
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IAssetRegistry<int>? _registry = null;
[RegisterExportedCollection(nameof(_registry), "Registry")]
public List<IntConfig>? Values { get; } = new();
}
}
""";
const string expected = """
// <auto-generated />
@ -243,7 +275,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
[Test]
@ -302,33 +334,51 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0)
.WithArguments("Register", "_registry", "Values"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
{
string source = CreateSource(
"""
public class BaseRegistry
{
public void Register(int value) { }
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
public sealed class DerivedRegistry : BaseRegistry
{
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly DerivedRegistry? _registry = new();
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true);
namespace TestApp
{
public class BaseRegistry
{
public void Register(int value) { }
}
public sealed class DerivedRegistry : BaseRegistry
{
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly DerivedRegistry? _registry = new();
[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
public List<int>? Values { get; } = new();
}
}
""";
const string expected = """
// <auto-generated />
@ -354,32 +404,50 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
[Test]
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
public abstract class BootstrapperBase
{
protected readonly IntRegistry? _registry = new();
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AutoRegisterExportedCollections]
public partial class Bootstrapper : BootstrapperBase
{
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true);
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry
{
public void Register(int value) { }
}
public abstract class BootstrapperBase
{
protected readonly IntRegistry? _registry = new();
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper : BootstrapperBase
{
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
}
""";
const string expected = """
// <auto-generated />
@ -405,47 +473,74 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
[Test]
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
const string source = """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry _registry = new();
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public static List<int> {|#0:StaticValues|} = new();
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public static List<int> {|#1:StaticPropertyValues|} { get; } = new();
namespace TestApp
{
public sealed class IntRegistry
{
public void Register(int value) { }
}
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int> {|#2:WriteOnlyValues|} { set { } }
}
""");
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry _registry = new();
await VerifyDiagnosticsAsync(
source,
skipGeneratedSourcesCheck: true,
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("StaticValues"),
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(1)
.WithArguments("StaticPropertyValues"),
new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(2)
.WithArguments("WriteOnlyValues")).ConfigureAwait(false);
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public static List<int> {|#0:StaticValues|} = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public static List<int> {|#1:StaticPropertyValues|} { get; } = new();
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int> {|#2:WriteOnlyValues|} { set { } }
}
}
""";
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("StaticValues"));
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(1)
.WithArguments("StaticPropertyValues"));
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_006", DiagnosticSeverity.Error)
.WithLocation(2)
.WithArguments("WriteOnlyValues"));
await test.RunAsync();
}
[Test]
@ -500,7 +595,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0)
.WithArguments("_registry", "Values"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
@ -555,7 +650,7 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0)
.WithArguments("Register", "_registry", "Values"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
@ -610,34 +705,51 @@ public class AutoRegisterExportedCollectionsGeneratorTests
.WithLocation(0)
.WithArguments("Values"));
await test.RunAsync().ConfigureAwait(false);
await test.RunAsync();
}
[Test]
public async Task Generates_Only_One_Source_When_Multiple_Partial_Declarations_Are_Annotated()
{
string source = CreateSource(
"""
public sealed class IntRegistry
{
public void Register(int value) { }
}
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry? _registry = new();
}
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class AutoRegisterExportedCollectionsAttribute : Attribute { }
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
""",
nullableEnabled: true,
allowMultipleDeclarations: true);
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}
namespace TestApp
{
public sealed class IntRegistry
{
public void Register(int value) { }
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IntRegistry? _registry = new();
}
[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
}
""";
const string expected = """
// <auto-generated />
@ -663,61 +775,6 @@ public class AutoRegisterExportedCollectionsGeneratorTests
await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected)).ConfigureAwait(false);
}
private static string CreateSource(
string applicationSource,
bool nullableEnabled = false,
bool allowMultipleDeclarations = false)
{
string nullableDirective = nullableEnabled ? "#nullable enable\n" : string.Empty;
string attributeDeclarations = allowMultipleDeclarations
? MultiDeclarationAttributeDeclarations
: StandardAttributeDeclarations;
return $$"""
{{nullableDirective}}using System;
using System.Collections;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
namespace GFramework.Godot.SourceGenerators.Abstractions.UI
{
{{attributeDeclarations}}
}
namespace TestApp
{
{{applicationSource}}
}
""";
}
private static async Task VerifyDiagnosticsAsync(
string source,
bool skipGeneratedSourcesCheck = false,
params DiagnosticResult[] expectedDiagnostics)
{
var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" }
};
if (skipGeneratedSourcesCheck)
{
test.TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck;
}
foreach (DiagnosticResult expectedDiagnostic in expectedDiagnostics)
{
test.ExpectedDiagnostics.Add(expectedDiagnostic);
}
await test.RunAsync().ConfigureAwait(false);
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
}

View File

@ -72,8 +72,19 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
if (bindNodeSignalAttribute is null || godotNodeSymbol is null)
return;
var methodAttributes = BuildMethodAttributeMap(candidates, bindNodeSignalAttribute);
var methodCandidates = CollectMethodCandidates(methodAttributes);
// 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。
var methodAttributes = candidates
.Where(static candidate => candidate is not null)
.Select(static candidate => candidate!)
.ToDictionary(
static candidate => candidate,
candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute),
ReferenceEqualityComparer.Instance);
var methodCandidates = methodAttributes
.Where(static pair => pair.Value.Count > 0)
.Select(static pair => pair.Key)
.ToList();
foreach (var group in GroupByContainingType(methodCandidates))
{
@ -88,7 +99,19 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
UnbindMethodName))
continue;
var bindings = CollectBindings(context, group, methodAttributes, godotNodeSymbol);
var bindings = new List<SignalBindingInfo>();
foreach (var candidate in group.Methods)
{
foreach (var attribute in methodAttributes[candidate])
{
if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding))
continue;
bindings.Add(binding);
}
}
if (bindings.Count == 0)
continue;
@ -148,22 +171,99 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
if (candidate.MethodSymbol.IsStatic)
{
ReportStaticMethodDiagnostic(context, candidate, attribute);
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.StaticMethodNotSupported,
candidate,
attribute,
candidate.MethodSymbol.Name);
return false;
}
if (!TryResolveBindingTargetNames(context, candidate, attribute, out var nodeFieldName, out var signalName))
if (!TryResolveCtorString(attribute, 0, out var nodeFieldName))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.InvalidConstructorArgument,
candidate,
attribute,
candidate.MethodSymbol.Name,
"nodeFieldName");
return false;
}
if (!TryFindCompatibleField(context, candidate, attribute, godotNodeSymbol, nodeFieldName, out var fieldSymbol))
if (!TryResolveCtorString(attribute, 1, out var signalName))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.InvalidConstructorArgument,
candidate,
attribute,
candidate.MethodSymbol.Name,
"signalName");
return false;
}
if (!TryFindCompatibleEvent(context, candidate, attribute, fieldSymbol, signalName, out var eventSymbol))
var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName);
if (fieldSymbol is null)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.NodeFieldNotFound,
candidate,
attribute,
candidate.MethodSymbol.Name,
nodeFieldName,
candidate.MethodSymbol.ContainingType.Name);
return false;
}
if (fieldSymbol.IsStatic)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField,
candidate,
attribute,
candidate.MethodSymbol.Name,
fieldSymbol.Name);
return false;
}
if (!fieldSymbol.Type.IsAssignableTo(godotNodeSymbol))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode,
candidate,
attribute,
fieldSymbol.Name);
return false;
}
var eventSymbol = FindEvent(fieldSymbol.Type, signalName);
if (eventSymbol is null)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.SignalNotFound,
candidate,
attribute,
fieldSymbol.Name,
signalName);
return false;
}
if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol))
{
ReportIncompatibleSignatureDiagnostic(context, candidate, attribute, eventSymbol, fieldSymbol);
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.MethodSignatureNotCompatible,
candidate,
attribute,
candidate.MethodSymbol.Name,
eventSymbol.Name,
fieldSymbol.Name);
return false;
}
@ -171,235 +271,6 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
return true;
}
private static Dictionary<MethodCandidate, IReadOnlyList<AttributeData>> BuildMethodAttributeMap(
ImmutableArray<MethodCandidate?> candidates,
INamedTypeSymbol bindNodeSignalAttribute)
{
return candidates
.Where(static candidate => candidate is not null)
.Select(static candidate => candidate!)
.ToDictionary(
static candidate => candidate,
candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute),
ReferenceEqualityComparer.Instance);
}
private static List<MethodCandidate> CollectMethodCandidates(
IReadOnlyDictionary<MethodCandidate, IReadOnlyList<AttributeData>> methodAttributes)
{
return methodAttributes
.Where(static pair => pair.Value.Count > 0)
.Select(static pair => pair.Key)
.ToList();
}
private static List<SignalBindingInfo> CollectBindings(
SourceProductionContext context,
TypeGroup group,
IReadOnlyDictionary<MethodCandidate, IReadOnlyList<AttributeData>> methodAttributes,
INamedTypeSymbol godotNodeSymbol)
{
var bindings = new List<SignalBindingInfo>();
foreach (var candidate in group.Methods)
{
foreach (var attribute in methodAttributes[candidate])
{
if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding))
continue;
bindings.Add(binding);
}
}
return bindings;
}
private static void ReportStaticMethodDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.StaticMethodNotSupported,
candidate,
attribute,
candidate.MethodSymbol.Name);
}
private static bool TryResolveBindingTargetNames(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
out string nodeFieldName,
out string signalName)
{
nodeFieldName = string.Empty;
signalName = string.Empty;
if (!TryResolveCtorString(attribute, 0, out nodeFieldName))
{
ReportInvalidConstructorArgumentDiagnostic(context, candidate, attribute, "nodeFieldName");
return false;
}
if (!TryResolveCtorString(attribute, 1, out signalName))
{
ReportInvalidConstructorArgumentDiagnostic(context, candidate, attribute, "signalName");
return false;
}
return true;
}
private static void ReportInvalidConstructorArgumentDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
string argumentName)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.InvalidConstructorArgument,
candidate,
attribute,
candidate.MethodSymbol.Name,
argumentName);
}
private static bool TryFindCompatibleField(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
INamedTypeSymbol godotNodeSymbol,
string nodeFieldName,
out IFieldSymbol fieldSymbol)
{
fieldSymbol = null!;
var resolvedField = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName);
if (resolvedField is null)
{
ReportNodeFieldNotFoundDiagnostic(context, candidate, attribute, nodeFieldName);
return false;
}
if (resolvedField.IsStatic)
{
ReportNodeFieldMustBeInstanceDiagnostic(context, candidate, attribute, resolvedField);
return false;
}
if (!resolvedField.Type.IsAssignableTo(godotNodeSymbol))
{
ReportFieldTypeMustDeriveFromNodeDiagnostic(context, candidate, attribute, resolvedField);
return false;
}
fieldSymbol = resolvedField;
return true;
}
private static void ReportNodeFieldNotFoundDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
string nodeFieldName)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.NodeFieldNotFound,
candidate,
attribute,
candidate.MethodSymbol.Name,
nodeFieldName,
candidate.MethodSymbol.ContainingType.Name);
}
private static void ReportNodeFieldMustBeInstanceDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
IFieldSymbol fieldSymbol)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField,
candidate,
attribute,
candidate.MethodSymbol.Name,
fieldSymbol.Name);
}
private static void ReportFieldTypeMustDeriveFromNodeDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
IFieldSymbol fieldSymbol)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode,
candidate,
attribute,
fieldSymbol.Name);
}
private static bool TryFindCompatibleEvent(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
IFieldSymbol fieldSymbol,
string signalName,
out IEventSymbol eventSymbol)
{
eventSymbol = null!;
var resolvedEvent = FindEvent(fieldSymbol.Type, signalName);
if (resolvedEvent is null)
{
ReportSignalNotFoundDiagnostic(context, candidate, attribute, fieldSymbol, signalName);
return false;
}
eventSymbol = resolvedEvent;
return true;
}
private static void ReportSignalNotFoundDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
IFieldSymbol fieldSymbol,
string signalName)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.SignalNotFound,
candidate,
attribute,
fieldSymbol.Name,
signalName);
}
private static void ReportIncompatibleSignatureDiagnostic(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
IEventSymbol eventSymbol,
IFieldSymbol fieldSymbol)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.MethodSignatureNotCompatible,
candidate,
attribute,
candidate.MethodSymbol.Name,
eventSymbol.Name,
fieldSymbol.Name);
}
private static void ReportMethodDiagnostic(
SourceProductionContext context,
DiagnosticDescriptor descriptor,
@ -533,7 +404,11 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
{
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.FirstOrDefault(method => IsParameterlessInstanceMethod(method, methodName));
.FirstOrDefault(method =>
method.Name == methodName &&
!method.IsStatic &&
method.Parameters.Length == 0 &&
method.MethodKind == MethodKind.Ordinary);
}
private static bool CallsGeneratedMethod(
@ -572,14 +447,6 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
};
}
private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName)
{
return string.Equals(method.Name, methodName, StringComparison.Ordinal) &&
!method.IsStatic &&
method.Parameters.Length == 0 &&
method.MethodKind == MethodKind.Ordinary;
}
private static bool IsBindNodeSignalAttributeName(NameSyntax attributeName)
{
var simpleName = GetAttributeSimpleName(attributeName);
@ -741,4 +608,4 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
return RuntimeHelpers.GetHashCode(obj);
}
}
}
}

View File

@ -259,7 +259,11 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
{
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.FirstOrDefault(static method => IsParameterlessInstanceMethod(method, "_Ready"));
.FirstOrDefault(static method =>
method.Name == "_Ready" &&
!method.IsStatic &&
method.Parameters.Length == 0 &&
method.MethodKind == MethodKind.Ordinary);
}
private static bool CallsGeneratedInjection(IMethodSymbol readyMethod)
@ -302,14 +306,6 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
return attribute.GetNamedArgument("Required", true);
}
private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName)
{
return string.Equals(method.Name, methodName, StringComparison.Ordinal) &&
!method.IsStatic &&
method.Parameters.Length == 0 &&
method.MethodKind == MethodKind.Ordinary;
}
private static bool TryResolvePath(
IFieldSymbol fieldSymbol,
AttributeData attribute,
@ -377,10 +373,7 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
if (!string.Equals(namedArgument.Key, "Lookup", StringComparison.Ordinal))
continue;
if (!string.Equals(
namedArgument.Value.Type?.ToDisplayString(),
GetNodeLookupModeMetadataName,
StringComparison.Ordinal))
if (namedArgument.Value.Type?.ToDisplayString() != GetNodeLookupModeMetadataName)
continue;
if (namedArgument.Value.Value is int value)
@ -575,4 +568,4 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
public List<FieldCandidate> Fields { get; } = new();
}
}
}

View File

@ -126,27 +126,7 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
var explicitMappings = new Dictionary<string, List<INamedTypeSymbol>>(StringComparer.Ordinal);
var implicitCandidates = new Dictionary<string, List<INamedTypeSymbol>>(StringComparer.Ordinal);
CollectMappingCandidates(
context,
typeCandidates,
autoLoadAttributeSymbol,
godotNodeSymbol,
projectAutoLoadNames,
explicitMappings,
implicitCandidates);
return ResolveTypedMappings(context, projectAutoLoadNames, explicitMappings, implicitCandidates);
}
private static void CollectMappingCandidates(
SourceProductionContext context,
IReadOnlyList<GodotTypeCandidate> typeCandidates,
INamedTypeSymbol? autoLoadAttributeSymbol,
INamedTypeSymbol godotNodeSymbol,
ISet<string> projectAutoLoadNames,
IDictionary<string, List<INamedTypeSymbol>> explicitMappings,
IDictionary<string, List<INamedTypeSymbol>> implicitCandidates)
{
foreach (var candidate in typeCandidates)
{
var typeSymbol = candidate.TypeSymbol;
@ -196,14 +176,7 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
explicitList.Add(typeSymbol);
}
}
private static Dictionary<string, INamedTypeSymbol> ResolveTypedMappings(
SourceProductionContext context,
IEnumerable<string> projectAutoLoadNames,
IReadOnlyDictionary<string, List<INamedTypeSymbol>> explicitMappings,
IReadOnlyDictionary<string, List<INamedTypeSymbol>> implicitCandidates)
{
var resolvedMappings = new Dictionary<string, INamedTypeSymbol>(StringComparer.Ordinal);
foreach (var projectAutoLoadName in projectAutoLoadNames.OrderBy(static name => name, StringComparer.Ordinal))
@ -435,40 +408,24 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
foreach (var member in members)
{
AppendAutoLoadMemberSource(builder, member);
builder.AppendLine(" /// <summary>");
builder.AppendLine($" /// 获取 AutoLoad <c>{member.AutoLoadName}</c>。");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine($" /// 尝试获取 AutoLoad <c>{member.AutoLoadName}</c>。");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public static bool TryGet{member.Identifier}(out {member.TypeName}? value)");
builder.AppendLine(" {");
builder.AppendLine(
$" return TryGetNode({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)}, out value);");
builder.AppendLine(" }");
builder.AppendLine();
}
AppendGetRequiredNodeSource(builder);
AppendTryGetNodeSource(builder);
builder.AppendLine("}");
return builder.ToString();
}
private static void AppendAutoLoadMemberSource(
StringBuilder builder,
GeneratedAutoLoadMember member)
{
builder.AppendLine(" /// <summary>");
builder.AppendLine($" /// 获取 AutoLoad <c>{member.AutoLoadName}</c>。");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine($" /// 尝试获取 AutoLoad <c>{member.AutoLoadName}</c>。");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public static bool TryGet{member.Identifier}(out {member.TypeName}? value)");
builder.AppendLine(" {");
builder.AppendLine(
$" return TryGetNode({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)}, out value);");
builder.AppendLine(" }");
builder.AppendLine();
}
private static void AppendGetRequiredNodeSource(StringBuilder builder)
{
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。");
builder.AppendLine(" /// </summary>");
@ -487,10 +444,6 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
" throw new global::System.InvalidOperationException($\"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.\");");
builder.AppendLine(" }");
builder.AppendLine();
}
private static void AppendTryGetNodeSource(StringBuilder builder)
{
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// 尝试从当前 SceneTree 根节点解析 AutoLoad。");
builder.AppendLine(" /// </summary>");
@ -517,6 +470,9 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
builder.AppendLine(" value = root.GetNodeOrNull<TNode>($\"/root/{autoLoadName}\");");
builder.AppendLine(" return value is not null;");
builder.AppendLine(" }");
builder.AppendLine("}");
return builder.ToString();
}
private static string GenerateInputActionsSource(IReadOnlyList<GeneratedInputActionMember> members)
@ -574,16 +530,45 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
if (string.IsNullOrWhiteSpace(content) || content.StartsWith(";", StringComparison.Ordinal))
continue;
if (TryUpdateSection(content, ref currentSection))
if (content.StartsWith("[", StringComparison.Ordinal) && content.EndsWith("]", StringComparison.Ordinal))
{
currentSection = content.Substring(1, content.Length - 2).Trim();
continue;
}
if (!TryParseAssignment(content, out var key, out var value))
continue;
if (TryCollectAutoLoadEntry(file, currentSection, key, value, seenAutoLoads, autoLoads, diagnostics))
continue;
if (string.Equals(currentSection, "autoload", StringComparison.OrdinalIgnoreCase))
{
if (!seenAutoLoads.Add(key))
{
diagnostics.Add(Diagnostic.Create(
GodotProjectDiagnostics.DuplicateAutoLoadEntry,
CreateFileLocation(file.Path),
key));
continue;
}
TryCollectInputAction(currentSection, key, seenInputActions, inputActions, diagnostics, file.Path);
autoLoads.Add(new ProjectAutoLoadEntry(
key,
NormalizeProjectPath(value)));
continue;
}
if (string.Equals(currentSection, "input", StringComparison.OrdinalIgnoreCase))
{
if (!seenInputActions.Add(key))
{
diagnostics.Add(Diagnostic.Create(
GodotProjectDiagnostics.DuplicateInputActionEntry,
CreateFileLocation(file.Path),
key));
continue;
}
inputActions.Add(key);
}
}
return new ProjectMetadataParseResult(
@ -593,68 +578,6 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
diagnostics.ToImmutableArray());
}
private static bool TryUpdateSection(string content, ref string currentSection)
{
if (!content.StartsWith("[", StringComparison.Ordinal) ||
!content.EndsWith("]", StringComparison.Ordinal))
{
return false;
}
currentSection = content.Substring(1, content.Length - 2).Trim();
return true;
}
private static bool TryCollectAutoLoadEntry(
AdditionalText file,
string currentSection,
string key,
string value,
ISet<string> seenAutoLoads,
ICollection<ProjectAutoLoadEntry> autoLoads,
ICollection<Diagnostic> diagnostics)
{
if (!string.Equals(currentSection, "autoload", StringComparison.OrdinalIgnoreCase))
return false;
if (!seenAutoLoads.Add(key))
{
diagnostics.Add(Diagnostic.Create(
GodotProjectDiagnostics.DuplicateAutoLoadEntry,
CreateFileLocation(file.Path),
key));
return true;
}
autoLoads.Add(new ProjectAutoLoadEntry(
key,
NormalizeProjectPath(value)));
return true;
}
private static void TryCollectInputAction(
string currentSection,
string key,
ISet<string> seenInputActions,
ICollection<string> inputActions,
ICollection<Diagnostic> diagnostics,
string filePath)
{
if (!string.Equals(currentSection, "input", StringComparison.OrdinalIgnoreCase))
return;
if (!seenInputActions.Add(key))
{
diagnostics.Add(Diagnostic.Create(
GodotProjectDiagnostics.DuplicateInputActionEntry,
CreateFileLocation(filePath),
key));
return;
}
inputActions.Add(key);
}
private static string NormalizeProjectPath(string rawValue)
{
var trimmed = rawValue.Trim();

View File

@ -183,15 +183,15 @@ public partial class MainMenu : Control
## 文档入口
- 生成器总览:[源码生成器总览](../docs/zh-CN/source-generators/index.md)
- Godot 项目元数据:[Godot 项目生成器](../docs/zh-CN/source-generators/godot-project-generator.md)
- `GetNode`[GetNode 生成器](../docs/zh-CN/source-generators/get-node-generator.md)
- `BindNodeSignal`[BindNodeSignal 生成器](../docs/zh-CN/source-generators/bind-node-signal-generator.md)
- `AutoScene`[AutoScene 生成器](../docs/zh-CN/source-generators/auto-scene-generator.md)
- `AutoUiPage`[AutoUiPage 生成器](../docs/zh-CN/source-generators/auto-ui-page-generator.md)
- `AutoRegisterExportedCollections`[AutoRegisterExportedCollections 生成器](../docs/zh-CN/source-generators/auto-register-exported-collections-generator.md)
- 生成器总览:[docs/zh-CN/source-generators/index.md](../docs/zh-CN/source-generators/index.md)
- Godot 项目元数据:[docs/zh-CN/source-generators/godot-project-generator.md](../docs/zh-CN/source-generators/godot-project-generator.md)
- `GetNode`[docs/zh-CN/source-generators/get-node-generator.md](../docs/zh-CN/source-generators/get-node-generator.md)
- `BindNodeSignal`[docs/zh-CN/source-generators/bind-node-signal-generator.md](../docs/zh-CN/source-generators/bind-node-signal-generator.md)
- `AutoScene`[docs/zh-CN/source-generators/auto-scene-generator.md](../docs/zh-CN/source-generators/auto-scene-generator.md)
- `AutoUiPage`[docs/zh-CN/source-generators/auto-ui-page-generator.md](../docs/zh-CN/source-generators/auto-ui-page-generator.md)
- `AutoRegisterExportedCollections`[docs/zh-CN/source-generators/auto-register-exported-collections-generator.md](../docs/zh-CN/source-generators/auto-register-exported-collections-generator.md)
- Godot 运行时入口:[../GFramework.Godot/README.md](../GFramework.Godot/README.md)
- 集成教程:[Godot 集成教程](../docs/zh-CN/tutorials/godot-integration.md)
- 集成教程:[docs/zh-CN/tutorials/godot-integration.md](../docs/zh-CN/tutorials/godot-integration.md)
## 什么时候不该先看这个包
@ -202,4 +202,4 @@ public partial class MainMenu : Control
- 你只需要 `Game` 契约,不需要 Godot 宿主或生成器:
- 先看 `GFramework.Game``GFramework.Game.Abstractions`
- 你在确认项目接线顺序,而不是单个生成器契约:
- 先看 [Godot 集成教程](../docs/zh-CN/tutorials/godot-integration.md)
- 先看 `docs/zh-CN/tutorials/godot-integration.md`

View File

@ -190,48 +190,6 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
{
registration = null!;
if (!TryResolveCollectionType(context, collectionMember, enumerableType, out var collectionType))
return false;
if (!TryResolveRegistryTarget(
context,
compilation,
ownerType,
collectionMember,
attribute,
out var registryMemberName,
out var registerMethodName,
out var registryType))
{
return false;
}
if (!TryResolveElementType(context, collectionMember, collectionType, out var elementType))
return false;
if (!HasCompatibleRegisterMethod(compilation, ownerType, registryType, registerMethodName, elementType))
{
context.ReportDiagnostic(Diagnostic.Create(
AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound,
collectionMember.Locations.FirstOrDefault() ?? Location.None,
registerMethodName,
registryMemberName,
collectionMember.Name));
return false;
}
registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName);
return true;
}
private static bool TryResolveCollectionType(
SourceProductionContext context,
ISymbol collectionMember,
INamedTypeSymbol enumerableType,
out ITypeSymbol collectionType)
{
collectionType = null!;
if (!IsInstanceReadableMember(collectionMember))
{
context.ReportDiagnostic(Diagnostic.Create(
@ -241,11 +199,17 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
var resolvedType = GetMemberType(collectionMember);
if (resolvedType is null)
var collectionType = collectionMember switch
{
IFieldSymbol field => field.Type,
IPropertySymbol property => property.Type,
_ => null
};
if (collectionType is null)
return false;
if (!resolvedType.IsAssignableTo(enumerableType))
if (!collectionType.IsAssignableTo(enumerableType))
{
context.ReportDiagnostic(Diagnostic.Create(
AutoRegisterExportedCollectionsDiagnostics.CollectionTypeMustBeEnumerable,
@ -254,35 +218,12 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
collectionType = resolvedType;
return true;
}
private static bool TryResolveRegistryTarget(
SourceProductionContext context,
Compilation compilation,
INamedTypeSymbol ownerType,
ISymbol collectionMember,
AttributeData attribute,
out string registryMemberName,
out string registerMethodName,
out INamedTypeSymbol registryType)
{
registryMemberName = string.Empty;
registerMethodName = string.Empty;
registryType = null!;
if (!TryGetRegistrationAttributeArguments(
context,
collectionMember,
attribute,
out registryMemberName,
out registerMethodName))
{
if (!TryGetRegistrationAttributeArguments(context, collectionMember, attribute, out var registryMemberName,
out var registerMethodName))
return false;
}
var registryMember = FindRegistryMember(ownerType, registryMemberName);
if (registryMember is null)
{
context.ReportDiagnostic(Diagnostic.Create(
@ -305,24 +246,18 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
var resolvedRegistryType = GetMemberType(registryMember) as INamedTypeSymbol;
if (resolvedRegistryType is null)
var registryType = registryMember switch
{
IFieldSymbol field => field.Type as INamedTypeSymbol,
IPropertySymbol property => property.Type as INamedTypeSymbol,
_ => null
};
if (registryType is null)
return false;
registryType = resolvedRegistryType;
return true;
}
private static bool TryResolveElementType(
SourceProductionContext context,
ISymbol collectionMember,
ITypeSymbol collectionType,
out ITypeSymbol elementType)
{
elementType = null!;
var resolvedElementType = TryGetElementType(collectionType);
if (resolvedElementType is null)
var elementType = TryGetElementType(collectionType);
if (elementType is null)
{
// Non-generic IEnumerable exposes elements as object at compile time, which is not safe
// for validating or generating a strongly typed registry call.
@ -333,33 +268,26 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener
return false;
}
elementType = resolvedElementType;
return true;
}
private static bool HasCompatibleRegisterMethod(
Compilation compilation,
INamedTypeSymbol ownerType,
INamedTypeSymbol registryType,
string registerMethodName,
ITypeSymbol elementType)
{
return EnumerateCandidateMethods(registryType, registerMethodName)
var hasCompatibleMethod = EnumerateCandidateMethods(registryType, registerMethodName)
.Any(method =>
!method.IsStatic &&
method.Parameters.Length == 1 &&
compilation.IsSymbolAccessibleWithin(method, ownerType) &&
CanAcceptElementType(compilation, elementType, method.Parameters[0].Type));
}
private static ITypeSymbol? GetMemberType(ISymbol member)
{
return member switch
if (!hasCompatibleMethod)
{
IFieldSymbol field => field.Type,
IPropertySymbol property => property.Type,
_ => null
};
context.ReportDiagnostic(Diagnostic.Create(
AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound,
collectionMember.Locations.FirstOrDefault() ?? Location.None,
registerMethodName,
registryMemberName,
collectionMember.Name));
return false;
}
registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName);
return true;
}
private static bool IsInstanceReadableMember(ISymbol member)

View File

@ -7,6 +7,7 @@
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup>

View File

@ -139,14 +139,14 @@ Godot 上。
## 文档入口
- Godot 运行时总览:[Godot 模块总览](../docs/zh-CN/godot/index.md)
- 架构集成:[Godot 架构集成](../docs/zh-CN/godot/architecture.md)
- 场景系统:[Godot 场景系统](../docs/zh-CN/godot/scene.md)
- UI 系统:[Godot UI 系统](../docs/zh-CN/godot/ui.md)
- 节点扩展:[Godot 节点扩展](../docs/zh-CN/godot/extensions.md)
- 信号系统:[Godot 信号系统](../docs/zh-CN/godot/signal.md)
- 日志系统:[Godot 日志系统](../docs/zh-CN/godot/logging.md)
- 集成教程:[Godot 集成教程](../docs/zh-CN/tutorials/godot-integration.md)
- Godot 运行时总览:[docs/zh-CN/godot/index.md](../docs/zh-CN/godot/index.md)
- 架构集成:[docs/zh-CN/godot/architecture.md](../docs/zh-CN/godot/architecture.md)
- 场景系统:[docs/zh-CN/godot/scene.md](../docs/zh-CN/godot/scene.md)
- UI 系统:[docs/zh-CN/godot/ui.md](../docs/zh-CN/godot/ui.md)
- 节点扩展:[docs/zh-CN/godot/extensions.md](../docs/zh-CN/godot/extensions.md)
- 信号系统:[docs/zh-CN/godot/signal.md](../docs/zh-CN/godot/signal.md)
- 日志系统:[docs/zh-CN/godot/logging.md](../docs/zh-CN/godot/logging.md)
- 集成教程:[docs/zh-CN/tutorials/godot-integration.md](../docs/zh-CN/tutorials/godot-integration.md)
- 生成器入口:[../GFramework.Godot.SourceGenerators/README.md](../GFramework.Godot.SourceGenerators/README.md)
## 什么时候不该把它当成主入口

View File

@ -11,9 +11,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Generic;
namespace GFramework.Godot.Setting.Data;
/// <summary>
@ -23,47 +20,24 @@ public class LocalizationMap
{
private const string DefaultFrameworkLanguage = "eng";
private const string DefaultGodotLocale = "en";
private readonly Dictionary<string, string> _frameworkLanguageMap;
private readonly Dictionary<string, string> _languageMap;
/// <summary>
/// 使用默认的 Godot locale 与框架语言码映射初始化本地化设置
/// 用户语言 -> Godot locale 映射表
/// </summary>
public LocalizationMap()
: this(CreateDefaultLanguageMap(), CreateDefaultFrameworkLanguageMap())
public Dictionary<string, string> LanguageMap { get; set; } = new(StringComparer.Ordinal)
{
}
{ "简体中文", "zh_CN" },
{ "English", "en" }
};
/// <summary>
/// 使用外部提供的映射初始化本地化设置。
/// 构造函数会复制输入字典,避免调用方在实例创建后继续修改内部状态。
/// 用户语言 -> GFramework 本地化语言码映射表。
/// </summary>
/// <param name="languageMap">用户语言到 Godot locale 的映射。</param>
/// <param name="frameworkLanguageMap">用户语言到 GFramework 本地化语言码的映射。</param>
/// <exception cref="ArgumentNullException">
/// 当 <paramref name="languageMap" /> 或 <paramref name="frameworkLanguageMap" /> 为 <see langword="null" /> 时抛出。
/// </exception>
public LocalizationMap(
IReadOnlyDictionary<string, string> languageMap,
IReadOnlyDictionary<string, string> frameworkLanguageMap)
public Dictionary<string, string> FrameworkLanguageMap { get; set; } = new(StringComparer.Ordinal)
{
ArgumentNullException.ThrowIfNull(languageMap);
ArgumentNullException.ThrowIfNull(frameworkLanguageMap);
// 复制外部输入,避免公共属性把可变集合直接暴露给调用方。
_languageMap = new Dictionary<string, string>(languageMap, StringComparer.Ordinal);
_frameworkLanguageMap = new Dictionary<string, string>(frameworkLanguageMap, StringComparer.Ordinal);
}
/// <summary>
/// 获取用户语言到 Godot locale 的只读映射表。
/// </summary>
public IReadOnlyDictionary<string, string> LanguageMap => _languageMap;
/// <summary>
/// 获取用户语言到 GFramework 本地化语言码的只读映射表。
/// </summary>
public IReadOnlyDictionary<string, string> FrameworkLanguageMap => _frameworkLanguageMap;
{ "简体中文", "zhs" },
{ "English", "eng" }
};
/// <summary>
/// 解析用户保存的语言值对应的 Godot locale。
@ -94,22 +68,4 @@ public class LocalizationMap
return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage);
}
private static Dictionary<string, string> CreateDefaultLanguageMap()
{
return new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "简体中文", "zh_CN" },
{ "English", "en" }
};
}
private static Dictionary<string, string> CreateDefaultFrameworkLanguageMap()
{
return new Dictionary<string, string>(StringComparer.Ordinal)
{
{ "简体中文", "zhs" },
{ "English", "eng" }
};
}
}

View File

@ -8,7 +8,7 @@
<Copyright>Copyright © 2025</Copyright>
<RepositoryUrl>https://github.com/GeWuYou/GFramework</RepositoryUrl>
<PackageProjectUrl>https://github.com/GeWuYou/GFramework</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>game;framework</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@ -16,7 +16,6 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<IncludeBuildOutput>false</IncludeBuildOutput>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<!-- This package is a pure meta-package that only aggregates dependencies. -->
<NoPackageAnalysis>false</NoPackageAnalysis>
</PropertyGroup>

View File

@ -11,8 +11,8 @@
## 从哪里开始
- 第一次接触框架:[入门指南](docs/zh-CN/getting-started/index.md)
- 想先跑一个最小例子:[快速开始](docs/zh-CN/getting-started/quick-start.md)
- 第一次接触框架:[`docs/zh-CN/getting-started/index.md`](docs/zh-CN/getting-started/index.md)
- 想先跑一个最小例子:[`docs/zh-CN/getting-started/quick-start.md`](docs/zh-CN/getting-started/quick-start.md)
- 已经知道要用哪个模块:直接进入对应模块目录下的 `README.md`
## 模块地图
@ -39,27 +39,27 @@
| 目录 | 定位 | 跟随入口 |
| --- | --- | --- |
| `GFramework.Core.SourceGenerators.Abstractions` | `Core.SourceGenerators` 的内部契约层 | [Core.SourceGenerators 模块 README](GFramework.Core.SourceGenerators/README.md) |
| `GFramework.Godot.SourceGenerators.Abstractions` | `Godot.SourceGenerators` 的内部契约层 | [Godot.SourceGenerators 模块 README](GFramework.Godot.SourceGenerators/README.md) |
| `GFramework.SourceGenerators.Common` | 生成器家族共享的公共支撑代码 | [源码生成器总览](docs/zh-CN/source-generators/index.md) |
| `GFramework.Core.SourceGenerators.Abstractions` | `Core.SourceGenerators` 的内部契约层 | [GFramework.Core.SourceGenerators/README.md](GFramework.Core.SourceGenerators/README.md) |
| `GFramework.Godot.SourceGenerators.Abstractions` | `Godot.SourceGenerators` 的内部契约层 | [GFramework.Godot.SourceGenerators/README.md](GFramework.Godot.SourceGenerators/README.md) |
| `GFramework.SourceGenerators.Common` | 生成器家族共享的公共支撑代码 | [docs/zh-CN/source-generators/index.md](docs/zh-CN/source-generators/index.md) |
## 文档导航
仓库根 README 与文档站点保持同一套栏目命名:
- 入门指南:[入门指南](docs/zh-CN/getting-started/index.md)
- Core[Core](docs/zh-CN/core/index.md)
- Game[Game](docs/zh-CN/game/index.md)
- Godot[Godot](docs/zh-CN/godot/index.md)
- 教程:[教程](docs/zh-CN/tutorials/index.md)
- 源码生成器:[源码生成器](docs/zh-CN/source-generators/index.md)
- ECS[ECS](docs/zh-CN/ecs/index.md)
- 抽象接口:[抽象接口](docs/zh-CN/abstractions/index.md)
- 最佳实践:[最佳实践](docs/zh-CN/best-practices/index.md)
- API 参考:[API 参考](docs/zh-CN/api-reference/index.md)
- FAQ[常见问题](docs/zh-CN/faq.md)
- 故障排查:[故障排查](docs/zh-CN/troubleshooting.md)
- 贡献:[贡献指南](docs/zh-CN/contributing.md)
- 入门指南:[`docs/zh-CN/getting-started/index.md`](docs/zh-CN/getting-started/index.md)
- Core[`docs/zh-CN/core/index.md`](docs/zh-CN/core/index.md)
- Game[`docs/zh-CN/game/index.md`](docs/zh-CN/game/index.md)
- Godot[`docs/zh-CN/godot/index.md`](docs/zh-CN/godot/index.md)
- 教程:[`docs/zh-CN/tutorials/index.md`](docs/zh-CN/tutorials/index.md)
- 源码生成器:[`docs/zh-CN/source-generators/index.md`](docs/zh-CN/source-generators/index.md)
- ECS[`docs/zh-CN/ecs/index.md`](docs/zh-CN/ecs/index.md)
- 抽象接口:[`docs/zh-CN/abstractions/index.md`](docs/zh-CN/abstractions/index.md)
- 最佳实践:[`docs/zh-CN/best-practices/index.md`](docs/zh-CN/best-practices/index.md)
- API 参考:[`docs/zh-CN/api-reference/index.md`](docs/zh-CN/api-reference/index.md)
- FAQ[`docs/zh-CN/faq.md`](docs/zh-CN/faq.md)
- 故障排查:[`docs/zh-CN/troubleshooting.md`](docs/zh-CN/troubleshooting.md)
- 贡献:[`docs/zh-CN/contributing.md`](docs/zh-CN/contributing.md)
## 包选择
@ -146,7 +146,7 @@ GFramework.sln
提交功能或行为变更时,请把代码、测试和文档一起更新:
1. 先阅读对应模块目录下的 `README.md`
2. 如果改动影响采用路径、安装方式、公共 API 或目录结构,同时更新受影响的中文文档页面,必要时同步调整 [中文文档入口](docs/zh-CN/index.md)
2. 如果改动影响采用路径、安装方式、公共 API 或目录结构,同时更新 `docs/zh-CN/`
3. 对跨模块或多阶段任务,维护 `ai-plan/public/README.md` 与对应主题目录下的 tracking / trace
## 许可证

View File

@ -1,16 +0,0 @@
# Analyzer Warning Reduction 跟踪历史RP-042 至 RP-048
## 范围说明
本归档承接 `RP-042``RP-048` 的晚期 active todo 内容,保留当时围绕 warning-reduction batch、baseline 与构建入口讨论的阶段性结论。
## 归档摘要
- 曾记录 `origin/main` baseline、branch diff 文件数与行数,用于 `$gframework-batch-boot 75` 的批处理停点判断
- 曾记录 `UnifiedSettingsFile``UnifiedSettingsDataRepository``LocalizationMap``CqrsHandlerRegistryGeneratorTests` 的 warning-reduction 切片已提交到当前分支
- 曾记录 RP-048 时在仓库根目录执行 plain `dotnet build` 成功,结果为 `0 Warning(s)` / `0 Error(s)`
- 这些内容在 RP-049 之后不再保留在 active todo 中因为当前恢复入口应只聚焦“plain `dotnet build` 是否打印 warning”这个真值
## superseded by
- [analyzer-warning-reduction-tracking.md](../../todos/analyzer-warning-reduction-tracking.md)

View File

@ -1,16 +0,0 @@
# Analyzer Warning Reduction 追踪历史RP-042 至 RP-048
## 范围说明
本归档承接 `RP-042``RP-048` 的 late-stage trace保留 active trace 在被 RP-049 压缩前的关键执行背景。
## 归档摘要
- 记录了 warning-reduction batch 在 `origin/main` 基线上的 diff 指标与“接近 75 个文件时停止”的批处理语境
- 记录了对 plain `dotnet build` 与带参数构建命令的比较,以及当时对 warning 检查入口的整理过程
- 记录了 RP-048 已确认默认 `dotnet build` 成功且当前工作树无活动代码修改
- RP-049 之后,这些内容不再作为默认恢复入口,而改为保存在 archive 供历史追溯
## superseded by
- [analyzer-warning-reduction-trace.md](../../traces/analyzer-warning-reduction-trace.md)

View File

@ -2,74 +2,59 @@
## 目标
继续以“直接看构建输出、直接修构建 warning”为原则推进当前分支并保持 active recovery 文档只保留当前真值
继续以“优先低风险、保持行为兼容”为原则收敛当前仓库的 Meziantou analyzer warnings并确保 active recovery 入口保持精简、可恢复
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-052`
- 当前阶段:`Phase 52`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-042`
- 当前阶段:`Phase 42`
- 当前焦点:
- `2026-04-24` 本轮从当前 PR review 的未解决线程回切到 `GFramework.Game` / `GFramework.Godot.SourceGenerators.Tests`
- `UnifiedSettingsFile.Sections``CloneFile` fallback 已对齐为“可保留原 comparer 时保留,否则显式回退到 `StringComparer.Ordinal`”的文档与实现契约
- `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 `await test.RunAsync();` 已统一补齐 `.ConfigureAwait(false)`,并同步让 `VerifyDiagnosticsAsync` 内部消费异步等待
- 当前批次仍需避免混入与 analyzer-warning-reduction 无关的既有工作树改动
- 已于 `2026-04-24` 使用 `gframework-pr-review` 复核当前分支 PR #280latest-head review 仍有 `3` 条 open threads
- 本地确认这 `3` 条 open threads 均指向 `ai-plan` 文档:错误归档链接、`rp002-rp041` trace 混入 `RP-001` 段落,以及 active trace 的恢复信息失真
- 本轮按最小写集直接修正文档恢复入口,不再扩大 `GFramework.SourceGenerators.Tests` 的代码写集
- `RP-041` 验证完成时,分支相对 `origin/main` 的唯一变更文件数为 `4`;这说明继续只处理同一热点文件时,该指标增长会很慢
- `GFramework.SourceGenerators.Tests``RP-042``net10.0` Release build 中仍为 `10``MA0051` warning、`0` error剩余热点继续集中在 `CqrsHandlerRegistryGeneratorTests.cs`
## 当前状态摘要
- 已修正 `archive/todos/analyzer-warning-reduction-history-rp002-rp041.md` 中指向 `RP-001` 归档的相对链接,恢复历史入口可点击性
- 已从 `archive/traces/analyzer-warning-reduction-history-rp002-rp041.md` 中移除误混入的 `RP-001` 段落,确保文件名与内容范围一致
- 已刷新 active tracking / trace 的恢复点描述,使其反映当前仍待远端收敛的是文档类 review threads而不是已处理过的代码项
- PR #280 的 MegaLinter 仍显示 `dotnet-format` warning但测试报告为 `2156 passed / 0 failed`;该 warning 目前更像 CI 环境 restore / SDK 噪音,而不是本地代码行为回归
## 当前活跃事实
- 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值
- 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理clean `Release` build 从 9 个 warning 降至 0 个 warning
- 当前已确认解决的文件包括 `BindNodeSignalGenerator.cs``GetNodeGenerator.cs``GodotProjectMetadataGenerator.cs``Registration/AutoRegisterExportedCollectionsGenerator.cs`
- 本轮直接执行仓库根目录 `dotnet clean` 仍在 `ValidateSolutionConfiguration` 阶段失败,输出未提供具体 error 文本
- 本轮直接执行仓库根目录 `dotnet build` 成功,并给出 `1184 warning(s)` 的真实输出
- `GFramework.Godot.SourceGenerators.Tests` 已通过测试辅助模板抽取与 `ConfigureAwait(false)` 修正,当前 `Debug` / `Release` 构建均为 `0 Warning(s)`
- 本轮已验证 `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`,结果为 `Passed: 48`
- 本轮已把 PR #283 中仍打开的 `UnifiedSettingsDataRepository.cs` comparer 契约线程落到代码与 XML 注释,避免 fallback 语义继续依赖隐式默认 comparer
- 本轮已确认 `AutoRegisterExportedCollectionsGeneratorTests` 的 5 处裸 `await test.RunAsync();` 不是当前 Release build 告警来源,但仍作为 PR review 一致性项一并修正
- 当前主题仍保持 active因为 `GFramework.SourceGenerators.Tests` 尚有剩余 `MA0051` warning 需要决定是否继续推进
- 继续按“单文件单方法”节奏处理 `CqrsHandlerRegistryGeneratorTests.cs` 可以稳定消除 warning但不利于快速提高唯一变更文件数
- 当前 PR review 已没有新的 failed-test 信号;当前优先级是提交这轮 `ai-plan` 修正并等待远端 PR threads 收敛
## 当前风险
- 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0
- 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
- 仓库根目录 `dotnet clean` 目前仍然无法给出新的 clean 基线
- 缓解措施:若下一轮继续做整仓 warning reduction先定位 `dotnet clean` 的 solution-level 失败原因,或明确继续沿用用户确认的 `1193 warning(s)` clean 基线与本轮 `1184 warning(s)` direct build 观测值
- 当前 worktree 已存在与本批次无关的未提交改动
- 缓解措施:提交当前批次时只暂存 `GFramework.Godot.SourceGenerators.Tests` 与对应 `ai-plan` 文件,避免混入其他 topic 变更
- `GFramework.Game` 当前 `Release` build 仍带有既有 analyzer warning 基线
- 缓解措施:本轮仅验证改动未新增 `UnifiedSettingsDataRepository` / `UnifiedSettingsFile` 相关 warning若继续在该模块做 warning reduction需要另开切片处理现存基线
- warning 治理策略风险:如果用户仍以“唯一变更文件数接近 `75`”作为目标,继续深挖同一测试文件会让目标推进缓慢
- 缓解措施:下一轮先确认是继续压低 `MA0051` 基线,还是切换到新的文件写集
- WSL 构建环境风险:当前 worktree 的 .NET 定向验证仍需显式附带 `-p:RestoreFallbackFolders=`,并在沙箱外运行以规避命名管道 / socket 限制
- 缓解措施:后续所有 affected-project Release build 继续复用该参数组合
- source generator test warning 范围风险:一旦继续触达 `GFramework.SourceGenerators.Tests`,剩余 warning 会继续成为本轮完成条件的一部分
- 缓解措施:继续用最小写集和 warnings-only build 锁定范围
## 活跃文档
- 当前轮次归档:
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- 历史跟踪归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
- 历史 trace 归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
## 验证说明
- `dotnet clean`
- 结果:失败;停在 solution `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`,未输出更具体的 error 文本
- `dotnet build`
- 结果:成功;`1184 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj`
- 初始结果:成功;`24 Warning(s)``0 Error(s)`
- 本轮收尾结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 结果:成功;`Passed: 48``Failed: 0`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`533 Warning(s)``0 Error(s)`;模块仍存在既有 warning 基线,本轮 follow-up 仅处理 PR review 指向的 comparer 契约与测试异步等待一致性
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 结果:成功;`Passed: 48``Failed: 0`
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders=""`
- 结果:通过;重写了受 Windows fallback package folder 影响的测试项目资产文件
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`10 Warning(s)``0 Error(s)`warning 仍全部来自 `CqrsHandlerRegistryGeneratorTests.cs` 的既有 `MA0051` 基线
## 下一步建议
1. 提交当前 comparer 契约与 `ConfigureAwait(false)` PR follow-up并确认只纳入本 topic 相关文件
2. 视 PR review 反馈决定是否继续收敛 `GFramework.Game` 现有 warning 基线,或返回下一轮整仓 warning 热点筛选
1. 提交 `RP-042` 后重新抓取 PR #280 review确认这 `3` 条 latest-head open threads 是否随新提交收敛
2. 若 PR threads 收敛,再决定下一轮是继续清理 `CqrsHandlerRegistryGeneratorTests.cs` 的剩余 `MA0051`,还是切换到新的文件写集
3. 如果仍要继续沿用“唯一变更文件数接近 `75`”的目标,应优先切到新的 warning 写集,而不是继续深挖同一测试文件

View File

@ -1,91 +1,31 @@
# Analyzer Warning Reduction 追踪
# Analyzer Warning Reduction 追踪
## 2026-04-24 — RP-042
## 2026-04-24 — RP-052
### 阶段PR #280 review follow-up 与 ai-plan 恢复入口修正
### 阶段PR review follow-upcomparer 契约 + `ConfigureAwait(false)` 收尾)
- 触发背景:
- 当前分支 PR #283 的最新 review 中,`greptile-apps[bot]` 仍有一个未解决线程,指出 `UnifiedSettingsDataRepository.CloneFile` fallback 会静默丢失原 comparer
- CodeRabbit 另指出 `AutoRegisterExportedCollectionsGeneratorTests.cs` 中还残留 5 处 `await test.RunAsync();`,与同项目其他测试文件的 `.ConfigureAwait(false)` 风格不一致
- 主线程实施:
- 复核 PR review JSON、`UnifiedSettingsDataRepository.cs``UnifiedSettingsFile.cs``AutoRegisterExportedCollectionsGeneratorTests.cs` 的当前代码,确认只有 comparer 契约线程仍属最新 head 上的实质问题
- 将 `UnifiedSettingsFile.Sections` 的 XML 注释补充为显式 comparer 契约,并把默认字典初始化改为 `StringComparer.Ordinal`
- 将 `CloneFile` fallback 从隐式默认 comparer 改为显式 `StringComparer.Ordinal`,并同步修正文档注释,避免继续暗含“保留原语义”的错误表述
- 把 `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 5 处 `await test.RunAsync();` 统一为 `.ConfigureAwait(false)`,同时让 `VerifyDiagnosticsAsync` 内部也消费 `ConfigureAwait(false)`
- 验证里程碑:
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`533 Warning(s)``0 Error(s)``GFramework.Game` 仍有既有 warning 基线,本轮 follow-up 仅处理 PR review 指向的 comparer 契约与测试异步等待一致性
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 首次并行复验:失败;`FileNotFoundException`,原因是 `--no-build` 测试在 Release DLL 落盘前启动
- 串行复验:成功;`Passed: 48``Failed: 0`
- 启动复核:
- 使用 `gframework-pr-review` 抓取当前分支 PR #280 的 latest-head review threads、MegaLinter 摘要与测试报告
- 本地核对后确认 `3` 条 open threads 均仍成立,但全部集中在 `ai-plan` 文档恢复入口,而不是新的代码行为问题
- 决策:
- 不再继续扩大 `GFramework.SourceGenerators.Tests` 的写集,先把远端 latest-head review 中仍成立的文档问题全部收口
- 保持 `RP-042` 作为 active recovery point仅刷新其事实描述、归档链接和 trace 范围边界
- 实施调整:
- 修正 `archive/todos/analyzer-warning-reduction-history-rp002-rp041.md` 中两条指向 `RP-001` 归档的相对链接
- 从 `archive/traces/analyzer-warning-reduction-history-rp002-rp041.md` 中移除误混入的 `RP-001` 段落,使文件只保留 `RP-002``RP-041`
- 刷新 active tracking / trace 的恢复点描述,明确当前 open threads 已收敛为文档问题,并记录本轮 follow-up 的事实与下一步
- 验证结果:
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release -t:Rebuild --no-restore --disable-build-servers -m:1 -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`10 Warning(s)``0 Error(s)`warning 仍全部来自 `CqrsHandlerRegistryGeneratorTests.cs` 的既有 `MA0051` 基线
- 当前结论:
- PR #283 当前仍打开的 comparer review thread 已在本地代码与 XML 注释层面得到对应修复
- `AutoRegisterExportedCollectionsGeneratorTests` 的异步等待风格已与同项目其他测试保持一致
- 当前改动已通过直接受影响测试项目的 Release build 与串行 Release test 复验,可进入提交阶段
## 2026-04-24 — RP-051
### 阶段:`GFramework.Godot.SourceGenerators.Tests` warning 清零
- 触发背景:
- 用户要求直接运行 `dotnet clean`,不再添加额外 shell 包装solution-level `dotnet clean` 仍然在 `ValidateSolutionConfiguration` 阶段失败
- 直接执行仓库根目录 `dotnet build` 成功,并输出 `1184 warning(s)`,说明当前真实热点已从 `GFramework.Godot.SourceGenerators` 转移到对应测试项目
- 主线程实施:
- 以 `GFramework.Godot.SourceGenerators.Tests` 为独立批次,先确认该项目本地基线为 `24 warning(s)`
- 在 `BindNodeSignalGeneratorTests.cs``AutoSceneGeneratorTests.cs``AutoUiPageGeneratorTests.cs``GetNodeGeneratorTests.cs``AutoRegisterExportedCollectionsGeneratorTests.cs``GodotProjectMetadataGeneratorTests.cs` 中抽取共享 source / diagnostic helper压缩重复长方法
- 在 `Core/GeneratorTest.cs` 中补充 `ConfigureAwait(false)`,清除项目内唯一 `MA0004`
- 把 `GFramework.Godot.SourceGenerators.Tests` 项目 warning 从 `24` 降到 `0`
- 验证里程碑:
- `dotnet build`
- 结果:成功;`1184 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj`
- 初始结果:成功;`24 Warning(s)``0 Error(s)`
- 第一批(`BindNodeSignal` + `GeneratorTest`)后:`16 Warning(s)`
- 第二批(`AutoScene` / `AutoUiPage` / `GetNode`)后:`8 Warning(s)`
- 第三批(`Registration` / `Project`)后:`1 Warning(s)`
- 收尾修复后:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 结果:成功;`Passed: 48``Failed: 0`
- 当前结论:
- `GFramework.Godot.SourceGenerators.Tests` 已在 `Debug` / `Release` 构建下达到 `0 warning(s)`
- 按 `origin/main` merge-base 计算并只纳入当前暂存批次时,累计分支 diff 为 `23` 个文件,低于 `$gframework-batch-boot 75` 的主停止阈值
- 仓库根目录 `dotnet clean` 仍无法稳定产出新的 clean 基线,需要在下一轮单独排查
- 当前 worktree 已有与本批次无关的既有改动;提交时必须只暂存 analyzer warning reduction 相关文件
## 2026-04-24 — RP-050
### 阶段clean-build 基线修正与 `GFramework.Godot.SourceGenerators` 切片清零
- 触发背景:
- 用户确认之前的 `0 Warning(s)` 来自增量构建假阴性;只有先 `dotnet clean``dotnet build`warning 才会重新出现
- 用户给出 clean solution build 的真实结果:`Build succeeded with 1193 warning(s)`
- 主线程实施:
- 纠正当前 topic 的 active todo / trace把 clean build 作为新的 warning 检查真值
- 在 `BindNodeSignalGenerator.cs``GetNodeGenerator.cs``GodotProjectMetadataGenerator.cs` 中完成分阶段方法抽取与字符串比较修正
- 在 `Registration/AutoRegisterExportedCollectionsGenerator.cs` 中拆分 `TryCreateRegistration`,清除最后一个 `MA0051`
- 更新 `AGENTS.md`,明确 warning 检查必须先 `dotnet clean``dotnet build`
- 验证里程碑:
- `dotnet clean GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release`
- 首次验证:成功;`1 Warning(s)`,剩余 `Registration/AutoRegisterExportedCollectionsGenerator.cs(182,25)` `MA0051`
- 修复后复验:成功;`0 Warning(s)``0 Error(s)`
- 当前结论:
- `GFramework.Godot.SourceGenerators` 已在 clean `Release` build 下从 9 个 warning 降到 0 个 warning
- 整仓库 warning 基线仍以用户确认的 clean solution build `1193 warning(s)` 为准
- 下一轮应继续从 clean solution build 输出中选择新的低风险热点
- PR #280 当前没有 failed-test 回归信号latest-head review 剩余项可以全部在 `ai-plan` 范围内处理
- active 恢复入口与历史归档范围已重新对齐,后续 `boot` 不会再从 `rp002-rp041` 误读 `RP-001`
- 下一步建议:
- 提交后重新抓取 PR #280 review确认 open threads 是否收敛
- 若 threads 收敛,则回到 `CqrsHandlerRegistryGeneratorTests.cs` 剩余 `MA0051`,或根据目标改切新的 warning 写集
## Archive Context
- 当前轮次归档:
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
- 历史跟踪归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)

View File

@ -12,12 +12,12 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-031`
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-026`
- 当前阶段:`Phase 5 - Governance Maintenance`
- 当前焦点:
- 继续按 `$gframework-batch-boot 75``origin/main` 分支 diff 阈值做小批量文档治理;当前 baseline 已回到 `origin/main`,本批只继续处理新的低风险 reader-facing 缺
- 收口 PR `#282` 的 latest-head review follow-up保持 active tracking / trace 只承载当前恢复入
- 保持 `README.md``docs/**` 公开页面只承载读者需要的采用信息,不再混入 XML inventory、覆盖基线、恢复点或治理批次说明
- 继续优先处理低风险 metadata 缺口、坏链、README 文档入口对齐、Reader-friendly 链接标签与 Markdown 结构问题,避免跨模块语义改写
- 继续`$gframework-batch-boot 75``origin/main` 分支 diff 阈值做小批量文档治理,优先处理低风险 metadata 缺口、坏链与 Markdown 结构问题
- 保持 `Game` persistence docs surface 与当前 `README`、源码、`PersistenceTests` 使用同一套 owner / adoption path 叙述
- 保持 `GFramework.Godot.SourceGenerators/README.md``docs/zh-CN/tutorials/godot-integration.md` 在生命周期接法上的一致性
- 保持 active tracking / trace 只承载当前恢复入口,把阶段细节留在 `archive/`
@ -25,20 +25,12 @@
## 当前状态摘要
- `Core``Ecs.Arch``Cqrs``Game``Godot` 五个模块族当前都已有 README / landing / topic / API 参考层级的已验证入口。
- `2026-04-24` 当前本地 `docs/sdk-update-documentation``origin/main` 同步到 `4c2994e`,相对 `$gframework-batch-boot 75` 的 baseline 当前为 `0 / 75` 个 changed files后续批次可以继续但仍应保持小 write set。
- `2026-04-24` 使用 `$gframework-pr-review` 抓取当前 PR `#284` 后,确认 latest head commit
`77540c07f0890cc05b10a849722c87b8bed8f561` 仍有 `3` 条 CodeRabbit 与 `1` 条 Greptile open thread本轮仅继续收口本地复核后仍成立的 reader-facing 文档入口与 active tracking 精简问题。
- `2026-04-24` 使用 `$gframework-pr-review` 抓取 PR `#282` 后,确认 latest head commit
`982249151ecf8acdff3e62e664034bf95dfacd75` 当前仍有 `3` 条 CodeRabbit 与 `1` 条 Greptile open thread4 条建议均已在本地复核并纳入当前恢复点。
- 本轮 PR follow-up 仅收口仍然成立的 review 项:
- 将过长的 active tracking / trace 瘦身,并把 `RP-023``RP-025` 的细节迁入 `archive/`
- 将 `docs/zh-CN/core/context.md` 的标题本地化为中文读者友好的写法
- 统一 `docs/zh-CN/troubleshooting.md``/zh-CN/core/architecture``/zh-CN/faq``.md` 链接写法
- 本批次将根 `README.md` 中两个仍直接暴露文件路径的内部支撑模块入口改为 reader-friendly 链接标签,避免目录表格继续把路径本身当成入口名称。
- 本批次继续将 `Core``Game``Source Generators` 和三篇 `Abstractions` 落地页的纯英文 `title` / H1 改为中文读者友好的入口标题,减少首页与侧边栏扫描成本。
- 本批次继续将 `core/architecture.md``command.md``events.md``logging.md``property.md``query.md` 的纯英文 `title` / H1 本地化为中英对照入口标题,保持 Core 子栏目扫描体验一致。
- 当前批次完成后,纯英文 `title` 扫描只剩 `docs/zh-CN/core/cqrs.md``CQRS``docs/zh-CN/index.md``GFramework`;它们分别属于通用缩写与品牌名,不再作为本轮优先本地化对象。
- 本批次补齐了 `docs/zh-CN/index.md``description`,以及 `docs/zh-CN/tutorials/basic/01-07.md``title` / `description`,让首页和基础教程章节页拥有完整 frontmatter metadata。
- 本批次统一将教程、最佳实践、Core、Godot 页面里缺显式扩展名的站内 Markdown 链接补齐为 `.md``index.md`,避免目录链接、绝对路径旧写法与 VitePress 构建解析分叉。
- 本批次把模块 README、仓库根 README、`docs/index.md` 及多组中文落地页里直接暴露文件路径的入口调整为读者友好的可点击标签,同时补齐语言落地页 metadata 与 README 指向。
- `Game` persistence docs surface 当前以 `docs/zh-CN/game/data.md``storage.md``serialization.md``setting.md`
作为最小巡检集合;若后续 README、runtime public API 或 `PersistenceTests` 变动,应优先复核这一组页面。
- `Godot` runtime 与 generator 入口当前以 `GFramework.Godot/README.md`
@ -70,31 +62,18 @@
## 最新验证
- `2026-04-24` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果通过PR `#284` 处于 `OPEN`latest head commit `77540c07f0890cc05b10a849722c87b8bed8f561``3` 条 CodeRabbit 与 `1` 条 Greptile open thread测试汇总为 `2156 passed`,仅剩 `Title check` 的 inconclusive PR 元数据提示。
- `2026-04-24` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
- 结果通过PR `#282` 处于 `OPEN`latest head commit`3` 条 CodeRabbit 与 `1` 条 Greptile open thread测试汇总为 `2156 passed`,仅剩 `Title check` 的 inconclusive PR 元数据提示。
- `2026-04-24` `rg -n --pcre2 '\\]\\(/zh-CN/[^)]+(?<!\\.md)\\)' docs/zh-CN/troubleshooting.md`
- 结果:当前无命中;`/zh-CN/core/architecture``/zh-CN/faq` 已统一补成显式 `.md` 链接。
- `2026-04-24` `bun run build`(工作目录:`docs/`
- 结果:通过;文档标题本地化、站内链接修正与 `ai-plan` 归档瘦身落地后站点仍可正常构建,仅保留既有大 chunk warning。
- `2026-04-24` `python3 - <<'PY' ...`(扫描 `docs/zh-CN/**/*.md` frontmatter 是否缺 `title` / `description`
- 结果:通过;当前带 frontmatter 的 `docs/zh-CN` 页面已无 `title` / `description` 缺口。
- `2026-04-24` `bun run build`(工作目录:`docs/`
- 结果:通过;首页与基础教程 metadata 补齐后站点仍可正常构建,仅保留既有大 chunk warning。
- `2026-04-24` `python3 - <<'PY' ...`(扫描 `docs/zh-CN/**/*.md` 中以 `./``../``/zh-CN/` 开头且未带扩展名的 Markdown 链接)
- 结果:通过;当前 `docs/zh-CN` 站内 Markdown 链接已无缺失扩展名的命中。
- `2026-04-24` `bun run build`(工作目录:`docs/`
- 结果:通过;`25` 个页面的站内链接补齐为显式 `.md` / `index.md` 后站点仍可正常构建,仅保留既有大 chunk warning。
- `2026-04-24` `bun run build`(工作目录:`docs/`
- 结果:通过;模块 README、中文落地页 reader-facing 文档入口对齐,以及 `docs/index.md` metadata 调整后站点仍可正常构建,仅保留既有大 chunk warning。
- `2026-04-24` `python3 - <<'PY' ...`(扫描 `docs/zh-CN/**/*.md` 中纯英文 `title`
- 结果:通过;经过三轮标题本地化后,仅剩 `CQRS``GFramework` 两个品牌/缩写型标题。
- `2026-04-24` `bun run build`(工作目录:`docs/`
- 结果:通过;根 `README.md` reader-friendly 链接标签修正与 `docs/zh-CN` 多页标题本地化落地后站点仍可正常构建,仅保留既有大 chunk warning。
## 下一步
1. 当前基线已回到 `origin/main`,本轮变更仍是小 write set后续若继续执行 `$gframework-batch-boot 75`,仍优先选择 `5``10` 个文件以内的小批次。
2. 若继续处理 reader-facing 文档问题,优先筛查剩余 README / landing page 中是否还有路径式链接标签或不必要的内部治理措辞;纯英文标题方面仅剩品牌名与缩写,不再是当前高优先级切片。
1. 若继续执行 `$gframework-batch-boot 75`,优先处理 `docs/zh-CN/index.md``tutorials/basic/01-07.md``8`
个“已有 frontmatter 但缺 `title` / `description`”的 metadata 缺口。
2. 推送当前 follow-up commit 后,再次执行 `$gframework-pr-review`,确认 PR `#282` 的 unresolved review threads 是否已在新 head commit 上消失。
3. 若后续分支继续调整 `Game` persistence runtime、README 或公共 API优先复核 `docs/zh-CN/game/data.md`
`storage.md``serialization.md``setting.md` 与 landing page 是否仍保持同一套职责边界。
4. 若后续分支继续调整 `Godot` generator 接法,优先复核 `GFramework.Godot.SourceGenerators/README.md`

View File

@ -2,39 +2,32 @@
## 2026-04-24
### 当前恢复点RP-031
### 当前恢复点RP-026
- 当前批次聚焦新的低风险 reader-facing README 缺口,只处理根 `README.md` 的路径式链接标签和对应的 active tracking / trace 基线更新。
- 以 `origin/main``4c2994e``2026-04-24 17:57:23 +0800`)为 `$gframework-batch-boot 75` baseline当前本地 `docs/sdk-update-documentation` 与该基线同步branch cumulative diff 起始值为 `0 / 75`
- 本批次随后扩展到 `6` 个中文 landing / abstraction 页面标题本地化,总 write set 仍保持在 reader-facing 文案层。
- 使用 `$gframework-pr-review` 抓取 PR `#282`,确认 latest head commit
`982249151ecf8acdff3e62e664034bf95dfacd75` 当前仍有 `3` 条 CodeRabbit 与 `1` 条 Greptile open thread。
- 按“只处理 latest-head unresolved threads 中仍成立的问题”的原则,本轮仅收口 4 条本地可复现的 follow-up
- 将 `tracking.md``trace.md` 的活动入口瘦身,并把 `RP-023``RP-025` 的细节迁入新 archive 文件
- 将 `docs/zh-CN/core/context.md` 的标题与主标题本地化为 `上下文Context`
- 将 `docs/zh-CN/troubleshooting.md``/zh-CN/core/architecture``/zh-CN/faq` 统一补成显式 `.md` 链接
### 当前决策RP-031
### 当前决策RP-026
- 当当前分支重新与 `origin/main` 对齐后,`$gframework-batch-boot 75` 可以继续推进,但仍只接受低风险、读者可见、易验证的小批次
- 公开 README 的表格入口不应继续把文件路径本身暴露成链接标签;入口名称应直接告诉读者要进入哪个模块 README
- active tracking / trace 里的 stop-condition 指标必须反映当前真实基线,不再沿用已经过时的 `58 / 75` 历史值
- PR review follow-up 继续遵守“先本地验证,再决定是否修复”;对已经过时或无法在当前分支复现的评论不做追随式修改
- active `ai-plan` 入口只保留当前恢复点、活动事实、风险、最新验证与下一步;批次细节统一迁入 `archive/`
- `docs/zh-CN` 页面应优先使用中文标题;同一帮助块中的绝对站内链接应保持一致的显式 `.md` 写法
### 当前验证RP-031
### 当前验证RP-026
- 基线确认:
- `git --git-dir=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-update-documentation --work-tree=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-update-documentation for-each-ref --format='%(refname:short) %(objectname:short) %(committerdate:iso8601)' refs/heads/main refs/remotes/origin/main refs/heads/docs/sdk-update-documentation HEAD`
- 结果:通过;`docs/sdk-update-documentation``origin/main` 当前同为 `4c2994e`,本地 `main` 仍停留在 `84b40a2`,因此本轮按 skill 规则选择更新的 `origin/main` 作为 baseline。
- 当前 stop-condition metric
- `git ... diff --name-only origin/main...HEAD | wc -l`
- 结果:通过;当前 branch cumulative diff 为 `0 / 75`
- 热点扫描:
- `rg -n '\\[[^]]*(?:README\\.md|docs/[^]]+|GFramework\\.[^]]+/README\\.md|/zh-CN/[^]]+)\\]\\((?:README\\.md|docs/[^)]+|GFramework\\.[^)]+/README\\.md|/zh-CN/[^)]+)\\)' README.md docs GFramework.*/README.md`
- 结果:通过;当前仅在根 `README.md` 的内部支撑模块表格中命中 `2` 处路径式链接标签,适合作为本批次 reader-facing 修正切片。
- 英文标题扫描:
- `python3 - <<'PY' ...`(扫描 `docs/zh-CN/**/*.md` 中纯英文 `title` / H1
- 结果:通过;当前 landing / abstraction 页仍有一组纯英文入口标题,其中 `core/index.md``game/index.md``source-generators/index.md``abstractions/*.md``title` / H1 适合作为同批次标题本地化切片。
- 第二轮标题本地化:
- 同一轮扫描显示 `docs/zh-CN/core/architecture.md``command.md``events.md``logging.md``property.md``query.md` 仍保留纯英文 `title` / H1这些页面只需做中英对照标题调整不涉及正文结构或链接变更适合作为后续同批次切片。
- 停批判断:
- 再次扫描后,剩余纯英文 `title` 只剩 `docs/zh-CN/core/cqrs.md``CQRS``docs/zh-CN/index.md``GFramework`;它们属于缩写或品牌名,不再作为当前 reader-facing 本地化批次的优先对象。
- 构建验证:
- PR review 抓取:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
- 结果通过PR `#282` 处于 `OPEN`,最新 review 线程与测试状态已成功解析,测试汇总为 `2156 passed`
- 链接巡检:
- `rg -n --pcre2 '\\]\\(/zh-CN/[^)]+(?<!\\.md)\\)' docs/zh-CN/troubleshooting.md`
- 结果:当前无命中;`/zh-CN/core/architecture``/zh-CN/faq` 已统一补成显式 `.md` 链接。
- 站点构建:
- `bun run build`(工作目录:`docs/`
- 结果:通过;`README.md` 链接标签修正与 `docs/zh-CN` 标题本地化后站点仍可构建,仅保留既有大 chunk warning。
- 结果:通过;本轮文档与 `ai-plan` 调整后站点仍可正常构建,仅保留既有大 chunk warning。
### 归档指针
@ -46,5 +39,5 @@
### 下一步
1. 提交当前批次,保留根 `README.md` 入口标签修正、`docs/zh-CN` 标题本地化和 `ai-plan` 恢复点同步更新
2. 若继续下一轮 `$gframework-batch-boot 75`,优先重新扫描剩余 README / landing page 的路径式链接标签和内部治理措辞,而不是继续本地化品牌名或缩写标题
1. 推送当前 follow-up commit 后,再次执行 `$gframework-pr-review`,确认 PR `#282` 的 unresolved review threads 是否已在新 head commit 上消失
2. 若继续执行 `$gframework-batch-boot 75`,优先处理 `docs/zh-CN/index.md``tutorials/basic/01-07.md``8` 个“已有 frontmatter 但缺 `title` / `description`”的 metadata 缺口

View File

@ -1,7 +1,5 @@
---
---
layout: page
title: Language Selection
description: Redirects visitors to the current Chinese documentation entry and keeps the language landing page discoverable.
---
<script setup>
@ -41,4 +39,4 @@ onMounted(() => {
<p style="margin-top: 40px; color: #666;">
Auto-redirecting... / 自动跳转中...
</p>
</div>
</div>

View File

@ -1,9 +1,9 @@
---
title: Core 抽象层
title: Core Abstractions
description: GFramework.Core.Abstractions 的契约边界、包关系与源码阅读重点。
---
# Core 抽象层
# Core Abstractions
`GFramework.Core.Abstractions``Core` 运行时的契约包。
@ -98,6 +98,6 @@ public sealed class DiagnosticsFeature
1. 先读本页,确认你是否真的只需要契约层
2. 再看 [`../core/index.md`](../core/index.md) 了解默认运行时怎么组织这些契约
3. 回到模块 README
- [`GFramework.Core.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [`GFramework.Core README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
- [`GFramework.Core.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [`GFramework.Core/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
4. 需要统一导航时,再看 [`../api-reference/index.md`](../api-reference/index.md)

View File

@ -1,9 +1,9 @@
---
title: Ecs.Arch 抽象层
title: Ecs.Arch Abstractions
description: GFramework.Ecs.Arch.Abstractions 的契约边界、包关系和最小接入路径。
---
# Ecs.Arch 抽象层
# Ecs.Arch Abstractions
`GFramework.Ecs.Arch.Abstractions` 是 Arch ECS 集成层的契约包。
@ -90,5 +90,5 @@ var options = new ArchOptions
1. 先读本页,确认你是否真的只需要契约层
2. 如果需要默认实现,再看 [`../ecs/arch.md`](../ecs/arch.md)
3. 回到对应模块 README
- [`GFramework.Ecs.Arch.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md)
- [`GFramework.Ecs.Arch README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)
- [`GFramework.Ecs.Arch.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md)
- [`GFramework.Ecs.Arch/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)

View File

@ -1,9 +1,9 @@
---
title: Game 抽象层
title: Game Abstractions
description: GFramework.Game.Abstractions 的契约边界、包关系与源码阅读重点。
---
# Game 抽象层
# Game Abstractions
`GFramework.Game.Abstractions``Game` 运行时的契约包。
@ -117,5 +117,5 @@ public sealed class ContinueGameCommandHandler
- [`../game/scene.md`](../game/scene.md)
- [`../game/ui.md`](../game/ui.md)
4. 需要仓库侧入口时,回到:
- [`GFramework.Game.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
- [`GFramework.Game README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
- [`GFramework.Game.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
- [`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)

View File

@ -29,11 +29,11 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
| 模块族 | 模块 README | 站内入口 | XML 文档关注点 |
| --- | --- | --- | --- |
| `Core` / `Core.Abstractions` | [`GFramework.Core README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)、[`GFramework.Core.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md) | [`Core 栏目`](../core/index.md)、[`Core 抽象层说明`](../abstractions/core-abstractions.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [`GFramework.Cqrs README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)、[`GFramework.Cqrs.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Abstractions/README.md)、[`GFramework.Cqrs.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md) | [`CQRS 栏目`](../core/cqrs.md)、[`CQRS Handler Registry 生成器`](../source-generators/cqrs-handler-registry-generator.md) | request / notification / handler / pipeline / registry / fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [`GFramework.Game README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)、[`GFramework.Game.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)、[`GFramework.Game.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.SourceGenerators/README.md) | [`Game 模块总览`](../game/index.md)、[`Game 抽象层说明`](../abstractions/game-abstractions.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
| `Godot` / `Godot.SourceGenerators` | [`GFramework.Godot README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)、[`GFramework.Godot.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md) | [`Godot 模块总览`](../godot/index.md)、[`Godot 项目生成器`](../source-generators/godot-project-generator.md)、[`GetNode 生成器`](../source-generators/get-node-generator.md)、[`BindNodeSignal 生成器`](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [`GFramework.Ecs.Arch README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)、[`GFramework.Ecs.Arch.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md) | [`ECS 模块总览`](../ecs/index.md)、[`Arch ECS 集成`](../ecs/arch.md)、[`Ecs.Arch 抽象层说明`](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
| `Core` / `Core.Abstractions` | [`GFramework.Core/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)、[`GFramework.Core.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md) | [`../core/index.md`](../core/index.md)、[`../abstractions/core-abstractions.md`](../abstractions/core-abstractions.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [`GFramework.Cqrs/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)、[`GFramework.Cqrs.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Abstractions/README.md)、[`GFramework.Cqrs.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md) | [`../core/cqrs.md`](../core/cqrs.md)、[`../source-generators/cqrs-handler-registry-generator.md`](../source-generators/cqrs-handler-registry-generator.md) | request / notification / handler / pipeline / registry / fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)、[`GFramework.Game.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)、[`GFramework.Game.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.SourceGenerators/README.md) | [`../game/index.md`](../game/index.md)、[`../abstractions/game-abstractions.md`](../abstractions/game-abstractions.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
| `Godot` / `Godot.SourceGenerators` | [`GFramework.Godot/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)、[`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md) | [`../godot/index.md`](../godot/index.md)、[`../source-generators/godot-project-generator.md`](../source-generators/godot-project-generator.md)、[`../source-generators/get-node-generator.md`](../source-generators/get-node-generator.md)、[`../source-generators/bind-node-signal-generator.md`](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [`GFramework.Ecs.Arch/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)、[`GFramework.Ecs.Arch.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md) | [`../ecs/index.md`](../ecs/index.md)、[`../ecs/arch.md`](../ecs/arch.md)、[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
## 先看 XML还是先看教程

View File

@ -3432,12 +3432,12 @@ public void GetPlayerStatsQuery_ShouldReturnCorrectStats()
### 相关资源
- [架构核心文档](/zh-CN/core/architecture.md)
- [命令模式文档](/zh-CN/core/command.md)
- [查询模式文档](/zh-CN/core/query.md)
- [事件系统文档](/zh-CN/core/events.md)
- [状态机文档](/zh-CN/core/state-machine.md)
- [IoC 容器文档](/zh-CN/core/ioc.md)
- [架构核心文档](/zh-CN/core/architecture)
- [命令模式文档](/zh-CN/core/command)
- [查询模式文档](/zh-CN/core/query)
- [事件系统文档](/zh-CN/core/events)
- [状态机文档](/zh-CN/core/state-machine)
- [IoC 容器文档](/zh-CN/core/ioc)
记住,好的架构不是一蹴而就的,需要持续的学习、实践和改进。

View File

@ -970,10 +970,10 @@ public class DeviceProfiler
## 相关文档
- [资源管理系统](/zh-CN/core/resource.md) - 资源管理详细说明
- [对象池系统](/zh-CN/core/pool.md) - 对象池优化
- [协程系统](/zh-CN/core/coroutine.md) - 异步操作优化
- [架构模式最佳实践](/zh-CN/best-practices/architecture-patterns.md) - 架构设计
- [资源管理系统](/zh-CN/core/resource) - 资源管理详细说明
- [对象池系统](/zh-CN/core/pool) - 对象池优化
- [协程系统](/zh-CN/core/coroutine) - 异步操作优化
- [架构模式最佳实践](/zh-CN/best-practices/architecture-patterns) - 架构设计
---

View File

@ -1,9 +1,9 @@
---
title: 架构(Architecture
title: Architecture
description: 说明 GFramework.Core 的 Architecture 入口、生命周期职责与最常用注册 API。
---
# 架构(Architecture
# Architecture
`Architecture``GFramework.Core` 的运行时入口。它负责三件事:

View File

@ -1,9 +1,9 @@
---
title: 命令(Command
title: Command
description: 说明 GFramework.Core.Command 旧命令体系的兼容定位、可用基类与当前使用约束。
---
# 命令(Command
# Command
本页只说明 `GFramework.Core.Command` 里的旧命令体系。

View File

@ -186,4 +186,4 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
- 架构入口:[architecture](./architecture.md)
- 上下文入口:[context](./context.md)
- 生成器专题:[../source-generators/cqrs-handler-registry-generator.md](../source-generators/cqrs-handler-registry-generator.md)
- 模块 README[`GFramework.Cqrs README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)
- 模块 README[`GFramework.Cqrs/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)

View File

@ -1,9 +1,9 @@
---
title: 事件(Events
title: Events
description: 说明 GFramework.Core.Events 的轻量广播模型、安装方式与常用事件入口。
---
# 事件(Events
# Events
`GFramework.Core.Events` 是架构内的轻量广播层。它适合表达“某件事已经发生”的运行时信号、模块间松耦合通知,
以及为旧模块保留 `EventBus` 语义;如果你需要请求/响应、pipeline behavior 或 handler registry优先使用

View File

@ -1,9 +1,9 @@
---
title: Core 模块
title: Core
description: GFramework.Core 与 GFramework.Core.Abstractions 的运行时入口、采用顺序和源码阅读导航。
---
# Core 模块
# Core
`Core` 栏目对应 `GFramework` 的基础运行时层,主要覆盖 `GFramework.Core``GFramework.Core.Abstractions`,以及与之直接相邻的旧版
`Command` / `Query` 执行器和新版 `CQRS` 迁移入口。
@ -71,7 +71,7 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
如果你已经知道模块归属,但想确认公开类型的契约边界,建议按下面顺序阅读:
1. 先看模块 README [`GFramework.Core`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md),确认包关系和目录边界
1. 先看模块 README [`GFramework.Core/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md),确认包关系和目录边界
2. 再看本栏目对应专题页,确认采用顺序、生命周期与推荐接线方式
3. 最后回到源码中的 XML 文档,重点核对这些类型族:
- `Architecture` / `IArchitectureContext`
@ -148,7 +148,7 @@ public sealed class CounterArchitecture : Architecture
## 对应模块入口
- [`GFramework.Core README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
- [`GFramework.Core.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [API 参考入口](../api-reference/index.md)
- [`GFramework.Core/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
- [`GFramework.Core.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [`docs/zh-CN/api-reference/index.md`](../api-reference/index.md)
- 仓库根 [`README.md`](https://github.com/GeWuYou/GFramework/blob/main/README.md)

View File

@ -1,9 +1,9 @@
---
title: 日志(Logging
title: Logging
description: 说明 GFramework.Core.Logging 的日志接口、组合方式与常见使用入口。
---
# 日志(Logging
# Logging
`GFramework.Core.Logging` 是 Core runtime 的默认日志实现。只加载抽象层时,`LoggerFactoryResolver` 会退回
silent provider加载 `GFramework.Core` 或在 `ArchitectureConfiguration` 里显式提供 provider 后,日志才会

View File

@ -1,9 +1,9 @@
---
title: 属性(Property
title: Property
description: 说明 GFramework.Core.Property 的可绑定属性模型、订阅方式与常见用法。
---
# 属性(Property
# Property
`GFramework.Core.Property` 负责字段级响应式值。它最适合“一个字段变化就足以驱动视图或局部业务逻辑”的场景;
如果你的状态已经是聚合状态树、需要 reducer / middleware / history再切到

View File

@ -1,9 +1,9 @@
---
title: 查询(Query
title: Query
description: 说明 GFramework.Core.Query 旧查询体系的兼容定位、可用基类与当前使用约束。
---
# 查询(Query
# Query
本页说明 `GFramework.Core.Query` 里的旧查询体系。

View File

@ -501,7 +501,7 @@ Parallel.For(0, 10, i =>
## 相关文档
- [对象池系统](/zh-CN/core/pool.md) - 结合对象池复用资源
- [协程系统](/zh-CN/core/coroutine.md) - 异步加载资源
- [Godot 扩展](/zh-CN/godot/extensions.md) - Godot 引擎的资源管理
- [资源管理最佳实践](/zh-CN/tutorials/resource-management.md) - 详细教程
- [对象池系统](/zh-CN/core/pool) - 结合对象池复用资源
- [协程系统](/zh-CN/core/coroutine) - 异步加载资源
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 引擎的资源管理
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 详细教程

View File

@ -576,7 +576,7 @@ await stateMachine.GoBackAsync();
## 相关文档
- [生命周期管理](/zh-CN/core/lifecycle.md) - 状态的初始化和销毁
- [事件系统](/zh-CN/core/events.md) - 状态变更通知
- [协程系统](/zh-CN/core/coroutine.md) - 异步状态操作
- [状态机实现教程](/zh-CN/tutorials/state-machine-tutorial.md) - 完整示例
- [生命周期管理](/zh-CN/core/lifecycle) - 状态的初始化和销毁
- [事件系统](/zh-CN/core/events) - 状态变更通知
- [协程系统](/zh-CN/core/coroutine) - 异步状态操作
- [状态机实现教程](/zh-CN/tutorials/state-machine-tutorial) - 完整示例

View File

@ -140,5 +140,5 @@ ecsModule.Update(deltaTime);
- ECS 模块总览:[`index.md`](./index.md)
- 抽象契约页:[`../abstractions/ecs-arch-abstractions.md`](../abstractions/ecs-arch-abstractions.md)
- 仓库模块 README[`GFramework.Ecs.Arch README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)
- 仓库模块 README[`GFramework.Ecs.Arch/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)
- 统一 API / XML 导航:[`../api-reference/index.md`](../api-reference/index.md)

View File

@ -1,9 +1,9 @@
---
title: Game 模块
title: Game
description: GFramework.Game family 的运行时入口、采用顺序与源码阅读导航。
---
# Game 模块
# Game
`Game` 栏目对应 `GFramework.Game``GFramework.Game.Abstractions` 这层游戏运行时能力。
@ -127,6 +127,6 @@ IStorage storage = new FileStorage("GameData", serializer);
## 对应模块入口
- [`GFramework.Game README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
- [`GFramework.Game.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
- [`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
- [`GFramework.Game.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
- 仓库根 [`README.md`](https://github.com/GeWuYou/GFramework/blob/main/README.md)

View File

@ -258,5 +258,5 @@ await sceneRouter.PopAsync();
1. [game/index.md](./index.md)
2. [ui.md](./ui.md)
3. [`GFramework.Game README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
4. [`GFramework.Game.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
3. [`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
4. [`GFramework.Game.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)

View File

@ -329,5 +329,5 @@ uiRouter.Hide(modalHandle, UiLayer.Modal);
1. [game/index.md](./index.md)
2. [scene.md](./scene.md)
3. [../source-generators/auto-ui-page-generator.md](../source-generators/auto-ui-page-generator.md)
4. [`GFramework.Game README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
5. [`GFramework.Game.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
4. [`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
5. [`GFramework.Game.Abstractions/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)

View File

@ -51,7 +51,7 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
对应文档:
- [`../core/cqrs.md`](../core/cqrs.md)
- 仓库内模块入口:[`GFramework.Cqrs README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)
- 仓库内模块入口:[`GFramework.Cqrs/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)
### 想做游戏运行时
@ -70,7 +70,7 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
对应文档:
- [`../game/index.md`](../game/index.md)
- 仓库内模块入口:[`GFramework.Game README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
- 仓库内模块入口:[`GFramework.Game/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
### 想接入 Godot
@ -81,7 +81,7 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
对应文档:
- [`../godot/index.md`](../godot/index.md)
- 仓库内模块入口:[`GFramework.Godot README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)
- 仓库内模块入口:[`GFramework.Godot/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)
## Source Generators 什么时候装

View File

@ -730,7 +730,7 @@ foreach (var reason in reasons)
## 相关文档
- [Godot 架构集成](/zh-CN/godot/architecture.md) - Godot 架构基础
- [Godot 场景系统](/zh-CN/godot/scene.md) - Godot 场景集成
- [Godot UI 系统](/zh-CN/godot/ui.md) - Godot UI 集成
- [Godot 扩展](/zh-CN/godot/extensions.md) - Godot 扩展方法
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法

View File

@ -678,7 +678,7 @@ GD.Print($"使用池: {stopwatch.ElapsedMilliseconds}ms");
## 相关文档
- [对象池系统](/zh-CN/core/pool.md) - 核心对象池实现
- [Godot 架构集成](/zh-CN/godot/architecture.md) - Godot 架构基础
- [Godot 场景系统](/zh-CN/godot/scene.md) - Godot 场景管理
- [性能优化](/zh-CN/core/pool.md) - 性能优化最佳实践
- [对象池系统](/zh-CN/core/pool) - 核心对象池实现
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景管理
- [性能优化](/zh-CN/core/pool) - 性能优化最佳实践

View File

@ -631,7 +631,7 @@ public class LazyResourceRepository<TKey, TResource>
## 相关文档
- [数据与存档系统](/zh-CN/game/data.md) - 数据持久化
- [Godot 架构集成](/zh-CN/godot/architecture.md) - Godot 架构基础
- [Godot 场景系统](/zh-CN/godot/scene.md) - 场景资源管理
- [资源管理系统](/zh-CN/core/resource.md) - 核心资源管理
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
- [Godot 场景系统](/zh-CN/godot/scene) - 场景资源管理
- [资源管理系统](/zh-CN/core/resource) - 核心资源管理

View File

@ -104,7 +104,7 @@ slider.Signal(Range.SignalName.ValueChanged)
### 4. 静态场景绑定优先交给 `[BindNodeSignal]`
[`GFramework.Godot.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)`ai-libs/CoreGrid` 的当前接法看,静态场景按钮、滑条、菜单项这类固定
`GFramework.Godot.SourceGenerators/README.md``ai-libs/CoreGrid` 的当前接法看,静态场景按钮、滑条、菜单项这类固定
节点,更常见的路径仍然是 `[BindNodeSignal]`
```csharp

View File

@ -2,7 +2,6 @@
# https://vitepress.dev/reference/default-theme-home-page
layout: home
title: GFramework
description: 概览 GFramework 的模块能力、入门路径与主要中文文档入口。
hero:
name: "GFramework"
text: 面向游戏开发的模块化 C# 架构体系
@ -36,4 +35,4 @@ features:
- title: ⚡ Roslyn 源码生成器
details: 自动生成日志、枚举扩展与规则代码,减少样板代码并提升开发效率。
---
---

View File

@ -220,4 +220,4 @@ public List<IntConfig>? Values { get; } = new();
1. [/zh-CN/source-generators/index](./index.md)
2. [/zh-CN/game/config-system](../game/config-system.md)
3. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
4. [`GFramework.Godot.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)
4. [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)

View File

@ -189,4 +189,4 @@ private void OnAnyButtonPressed()
1. [/zh-CN/source-generators/get-node-generator](./get-node-generator.md)
2. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
3. [/zh-CN/godot/ui](../godot/ui.md)
4. [`GFramework.Godot.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)
4. [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)

View File

@ -195,4 +195,4 @@ finally
1. [context-get-generator.md](./context-get-generator.md)
2. [logging-generator.md](./logging-generator.md)
3. [../core/index.md](../core/index.md)
4. [`GFramework.Core.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)
4. [`GFramework.Core.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)

View File

@ -195,4 +195,4 @@ public override void _Ready()
1. [/zh-CN/source-generators/bind-node-signal-generator](./bind-node-signal-generator.md)
2. [/zh-CN/source-generators/godot-project-generator](./godot-project-generator.md)
3. [/zh-CN/godot/ui](../godot/ui.md)
4. [`GFramework.Godot.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)
4. [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)

View File

@ -217,4 +217,4 @@ AutoLoad 名称也遵循同样的冲突处理策略。
1. [/zh-CN/source-generators/get-node-generator](./get-node-generator.md)
2. [/zh-CN/source-generators/bind-node-signal-generator](./bind-node-signal-generator.md)
3. [/zh-CN/tutorials/godot-integration](../tutorials/godot-integration.md)
4. [`GFramework.Godot.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)
4. [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)

View File

@ -1,9 +1,9 @@
---
title: 源码生成器
title: Source Generators
description: 按模块梳理 GFramework 当前发布的源码生成器包、运行时归属与推荐选包入口。
---
# 源码生成器
# Source Generators
`Source Generators` 栏目对应 `GFramework` 当前按模块拆分发布的编译期工具链。
@ -101,7 +101,7 @@ GFramework 当前发布的生成器包是:
## 对应模块入口
- [`GFramework.Core.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)
- [`GFramework.Game.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.SourceGenerators/README.md)
- [`GFramework.Cqrs.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md)
- [`GFramework.Godot.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)
- [`GFramework.Core.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)
- [`GFramework.Game.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.SourceGenerators/README.md)
- [`GFramework.Cqrs.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md)
- [`GFramework.Godot.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md)

View File

@ -213,4 +213,4 @@ public sealed class DynamicPrioritySystem : IPrioritized
1. [context-aware-generator.md](./context-aware-generator.md)
2. [context-get-generator.md](./context-get-generator.md)
3. [../core/index.md](../core/index.md)
4. [`GFramework.Core.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)
4. [`GFramework.Core.SourceGenerators/README.md`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.SourceGenerators/README.md)

View File

@ -1,6 +1,4 @@
---
title: 第 1 章:环境准备
description: 说明开始基础教程前需要准备的操作系统、.NET SDK、Godot 与常用开发工具环境。
prev:
text: '教程首页'
link: './index'

View File

@ -1,6 +1,4 @@
---
title: 第 2 章:项目创建与初始化
description: 指导创建 Godot C# 项目、引入 GFramework并搭好基础目录与初始化结构。
prev:
text: '环境准备'
link: './01-environment'

View File

@ -1,6 +1,4 @@
---
title: 第 3 章:基础计数器实现
description: 通过直接实现计数器场景与交互逻辑,建立后续架构重构的起点示例。
prev:
text: '项目创建与初始化'
link: './02-project-setup'

View File

@ -1,6 +1,4 @@
---
title: 第 4 章:引入 Model 重构
description: 使用 Model 层与事件系统重构计数器应用,分离状态存储、事件通知与界面更新。
prev:
text: '基础计数器实现'
link: './03-counter-basic'

View File

@ -1,6 +1,4 @@
---
title: 第 5 章:命令系统优化
description: 通过引入 Command 模式拆分 Controller 交互逻辑,建立更清晰的执行职责边界。
prev:
text: '引入 Model 重构'
link: './04-model-refactor'

View File

@ -1,6 +1,4 @@
---
title: 第 6 章Utility 与 System
description: 引入 Utility 与 System 承载业务规则和阈值检查,完成基础分层架构闭环。
prev:
text: '命令系统优化'
link: './05-command-system'

View File

@ -1,6 +1,4 @@
---
title: 第 7 章:总结与最佳实践
description: 回顾基础教程的架构演进,整理最佳实践、常见问题与后续学习方向。
prev:
text: 'Utility 与 System'
link: './06-utility-system'
@ -646,10 +644,10 @@ public override void _Process(double delta)
### 推荐资源
- **GFramework 文档**
- [Core 核心框架](../../core/index.md)
- [Game 游戏模块](../../game/index.md)
- [Godot 集成](../../godot/index.md)
- [源码生成器](../../source-generators/index.md)
- [Core 核心框架](../../core/)
- [Game 游戏模块](../../game/)
- [Godot 集成](../../godot/)
- [源码生成器](../../source-generators/)
- **设计模式**
- 命令模式Command Pattern

View File

@ -173,10 +173,10 @@ description: 从零开始串联环境准备、项目搭建与核心概念的基
## 🔗 相关资源
- [GFramework 文档首页](../../index.md)
- [Core 核心框架](../../core/index.md)
- [Godot 集成](../../godot/index.md)
- [源码生成器](../../source-generators/index.md)
- [GFramework 文档首页](../../)
- [Core 核心框架](../../core/)
- [Godot 集成](../../godot/)
- [源码生成器](../../source-generators/)
## ❓ 遇到问题?

View File

@ -188,5 +188,5 @@ BlinkCoroutine()
## 下一步
- Core 侧更完整的 API 说明见 [Core 协程系统](/zh-CN/core/coroutine.md)
- Godot 集成细节见 [Godot 协程系统](/zh-CN/godot/coroutine.md)
- Core 侧更完整的 API 说明见 [Core 协程系统](/zh-CN/core/coroutine)
- Godot 集成细节见 [Godot 协程系统](/zh-CN/godot/coroutine)

View File

@ -20,9 +20,9 @@ description: 学习如何实现数据版本迁移系统,处理不同版本间
- 已安装 GFramework.Game NuGet 包
- 了解 C# 基础语法和接口实现
- 阅读过[快速开始](/zh-CN/getting-started/quick-start.md)
- 了解[数据与存档系统](/zh-CN/game/data.md)
- 建议先完成[实现存档系统](/zh-CN/tutorials/save-system.md)教程
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[数据与存档系统](/zh-CN/game/data)
- 建议先完成[实现存档系统](/zh-CN/tutorials/save-system)教程
## 为什么需要数据迁移
@ -647,9 +647,9 @@ MyGame/
恭喜!你已经实现了一个完整的数据版本迁移系统。接下来可以学习:
- [实现存档系统](/zh-CN/tutorials/save-system.md) - 结合存档系统使用迁移
- [Godot 完整项目搭建](/zh-CN/tutorials/godot-complete-project.md) - 在实际项目中应用
- [数据与存档系统](/zh-CN/game/data.md) - 深入了解数据系统
- [实现存档系统](/zh-CN/tutorials/save-system) - 结合存档系统使用迁移
- [Godot 完整项目搭建](/zh-CN/tutorials/godot-complete-project) - 在实际项目中应用
- [数据与存档系统](/zh-CN/game/data) - 深入了解数据系统
## 最佳实践
@ -780,6 +780,6 @@ public async Task<List<ISettingsSection>> MigrateBatchAsync(
## 相关文档
- [数据与存档系统](/zh-CN/game/data.md) - 数据系统详细说明
- [实现存档系统](/zh-CN/tutorials/save-system.md) - 存档系统教程
- [架构系统](/zh-CN/core/architecture.md) - 架构设计原则
- [数据与存档系统](/zh-CN/game/data) - 数据系统详细说明
- [实现存档系统](/zh-CN/tutorials/save-system) - 存档系统教程
- [架构系统](/zh-CN/core/architecture) - 架构设计原则

View File

@ -20,7 +20,7 @@ description: 学习如何在实际项目中使用 Option、Result 和管道操
- 已安装 GFramework.Core NuGet 包
- 了解 C# 基础语法和泛型
- 阅读过[快速开始](/zh-CN/getting-started/quick-start.md)
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解 Lambda 表达式和 LINQ
## 步骤 1使用 Option 处理可空值
@ -811,12 +811,12 @@ class Program
恭喜!你已经掌握了函数式编程的核心技术。接下来可以学习:
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial.md) - 结合函数式编程和协程
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial.md) - 在状态机中应用函数式模式
- [资源管理最佳实践](/zh-CN/tutorials/resource-management.md) - 使用 Result 处理资源加载
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 结合函数式编程和协程
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 在状态机中应用函数式模式
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 使用 Result 处理资源加载
## 相关文档
- [扩展方法](/zh-CN/core/extensions.md) - 更多函数式扩展方法
- [架构组件](/zh-CN/core/architecture.md) - 在架构中使用函数式编程
- [最佳实践](/zh-CN/best-practices/architecture-patterns.md) - 函数式编程最佳实践
- [扩展方法](/zh-CN/core/extensions) - 更多函数式扩展方法
- [架构组件](/zh-CN/core/architecture) - 在架构中使用函数式编程
- [最佳实践](/zh-CN/best-practices/architecture-patterns) - 函数式编程最佳实践

View File

@ -22,9 +22,9 @@ description: 从零开始使用 GFramework 构建一个完整的 Godot 游戏项
- 已安装 .NET SDK 8.0+
- 了解 C# 和 Godot 基础
- 阅读过前面的教程:
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial.md)
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial.md)
- [实现存档系统](/zh-CN/tutorials/save-system.md)
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial)
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial)
- [实现存档系统](/zh-CN/tutorials/save-system)
## 项目概述
@ -805,7 +805,7 @@ public partial class GameScene : Node2D, IController
## 相关文档
- [Godot 架构集成](/zh-CN/godot/architecture.md) - 架构详细说明
- [Godot 场景系统](/zh-CN/godot/scene.md) - 场景管理
- [Godot UI 系统](/zh-CN/godot/ui.md) - UI 管理
- [Godot 扩展](/zh-CN/godot/extensions.md) - 扩展功能
- [Godot 架构集成](/zh-CN/godot/architecture) - 架构详细说明
- [Godot 场景系统](/zh-CN/godot/scene) - 场景管理
- [Godot UI 系统](/zh-CN/godot/ui) - UI 管理
- [Godot 扩展](/zh-CN/godot/extensions) - 扩展功能

View File

@ -9,7 +9,7 @@ description: 汇总 GFramework 的基础与进阶教程入口,帮助按学习
## 📚 可用教程
### [基础教程](./basic/index.md)
### [基础教程](./basic/)
> 从零开始学习 GFramework通过构建一个完整的计数器应用逐步掌握框架的核心概念。
@ -169,7 +169,7 @@ description: 汇总 GFramework 的基础与进阶教程入口,帮助按学习
### 善用资源
- **查阅文档**:结合 [Core 核心框架](../core/index.md) 和 [Godot 集成](../godot/index.md) 文档
- **查阅文档**:结合 [Core 核心框架](../core/) 和 [Godot 集成](../godot/) 文档
- **查看示例**:参考框架附带的示例项目
- **社区交流**:遇到问题时查看 GitHub Issues 或参与讨论
@ -193,11 +193,11 @@ description: 汇总 GFramework 的基础与进阶教程入口,帮助按学习
## 🔗 相关资源
- [入门指南](../getting-started/index.md) - 快速了解框架和安装配置
- [Core 核心框架](../core/index.md) - 深入学习核心概念
- [Game 模块](../game/index.md) - 游戏特定功能文档
- [Godot 集成](../godot/index.md) - Godot 特定功能参考
- [源码生成器](../source-generators/index.md) - 自动化代码生成工具
- [入门指南](../getting-started/) - 快速了解框架和安装配置
- [Core 核心框架](../core/) - 深入学习核心概念
- [Game 模块](../game/) - 游戏特定功能文档
- [Godot 集成](../godot/) - Godot 特定功能参考
- [源码生成器](../source-generators/) - 自动化代码生成工具
---
@ -236,7 +236,7 @@ A: 根据学习深度不同:
<div style="text-align: center; margin: 2rem 0;">
[开始基础教程 →](./basic/index.md)
[开始基础教程 →](./basic/)
</div>

View File

@ -20,8 +20,8 @@ description: 学习如何使用 GFramework 组织和管理大型游戏项目
- 已安装 GFramework.Core 和 GFramework.Game NuGet 包
- 了解 C# 基础语法和面向对象编程
- 阅读过[快速开始](/zh-CN/getting-started/quick-start.md)
- 了解[架构组件](/zh-CN/core/architecture.md)和[模块系统](/zh-CN/core/architecture.md#模块系统)
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[架构组件](/zh-CN/core/architecture)和[模块系统](/zh-CN/core/architecture#模块系统)
## 步骤 1: 项目结构设计
@ -1628,14 +1628,14 @@ namespace MyGame
恭喜!你已经掌握了大型项目的组织方法。接下来可以学习:
- [Godot 完整项目](/zh-CN/tutorials/godot-complete-project.md) - 在 Godot 中应用这些原则
- [资源管理最佳实践](/zh-CN/tutorials/resource-management.md) - 管理大型项目的资源
- [实现存档系统](/zh-CN/tutorials/save-system.md) - 保存复杂的游戏状态
- [架构模式最佳实践](/zh-CN/best-practices/architecture-patterns.md) - 高级架构模式
- [Godot 完整项目](/zh-CN/tutorials/godot-complete-project) - 在 Godot 中应用这些原则
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 管理大型项目的资源
- [实现存档系统](/zh-CN/tutorials/save-system) - 保存复杂的游戏状态
- [架构模式最佳实践](/zh-CN/best-practices/architecture-patterns) - 高级架构模式
## 相关文档
- [架构组件](/zh-CN/core/architecture.md) - 架构系统详解
- [模块系统](/zh-CN/core/architecture.md#模块系统) - 模块化设计
- [依赖注入](/zh-CN/core/ioc.md) - IoC 容器使用
- [最佳实践](/zh-CN/best-practices/index.md) - 开发最佳实践
- [架构组件](/zh-CN/core/architecture) - 架构系统详解
- [模块系统](/zh-CN/core/architecture#模块系统) - 模块化设计
- [依赖注入](/zh-CN/core/ioc) - IoC 容器使用
- [最佳实践](/zh-CN/best-practices/) - 开发最佳实践

View File

@ -20,8 +20,8 @@ description: 学习如何使用暂停系统实现多层暂停管理和游戏流
- 已安装 GFramework.Core NuGet 包
- 了解 C# 基础语法和接口实现
- 阅读过[快速开始](/zh-CN/getting-started/quick-start.md)
- 了解[架构组件](/zh-CN/core/architecture.md)基础
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[架构组件](/zh-CN/core/architecture)基础
## 步骤 1注册暂停管理器
@ -1120,14 +1120,14 @@ class Program
恭喜!你已经掌握了暂停系统的使用。接下来可以学习:
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial.md) - 在暂停状态下控制协程
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial.md) - 结合状态机管理游戏流程
- [事件系统](/zh-CN/core/events.md) - 使用事件响应暂停状态变化
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 在暂停状态下控制协程
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 结合状态机管理游戏流程
- [事件系统](/zh-CN/core/events) - 使用事件响应暂停状态变化
## 相关文档
- [架构组件](/zh-CN/core/architecture.md) - 架构基础
- [Utility 层](/zh-CN/core/utility.md) - Utility 详细说明
- [生命周期管理](/zh-CN/core/lifecycle.md) - 组件生命周期
- [扩展方法](/zh-CN/core/extensions.md) - 便捷的扩展方法
- [架构组件](/zh-CN/core/architecture) - 架构基础
- [Utility 层](/zh-CN/core/utility) - Utility 详细说明
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
- [扩展方法](/zh-CN/core/extensions) - 便捷的扩展方法

View File

@ -20,8 +20,8 @@ description: 学习如何高效管理游戏资源,避免内存泄漏和性能
- 已安装 GFramework.Core NuGet 包
- 了解 C# 基础语法和 async/await
- 阅读过[快速开始](/zh-CN/getting-started/quick-start.md)
- 了解[协程系统](/zh-CN/core/coroutine.md)
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[协程系统](/zh-CN/core/coroutine)
## 步骤 1创建资源类型和加载器
@ -802,13 +802,13 @@ MyGame/
恭喜!你已经掌握了资源管理的最佳实践。接下来可以学习:
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial.md) - 在协程中加载资源
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial.md) - 在状态切换时管理资源
- [实现存档系统](/zh-CN/tutorials/save-system.md) - 保存和加载游戏数据
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 在协程中加载资源
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 在状态切换时管理资源
- [实现存档系统](/zh-CN/tutorials/save-system) - 保存和加载游戏数据
## 相关文档
- [资源管理系统](/zh-CN/core/resource.md) - 资源系统详细说明
- [对象池系统](/zh-CN/core/pool.md) - 结合对象池复用资源
- [协程系统](/zh-CN/core/coroutine.md) - 异步加载资源
- [System 层](/zh-CN/core/system.md) - System 详细说明
- [资源管理系统](/zh-CN/core/resource) - 资源系统详细说明
- [对象池系统](/zh-CN/core/pool) - 结合对象池复用资源
- [协程系统](/zh-CN/core/coroutine) - 异步加载资源
- [System 层](/zh-CN/core/system) - System 详细说明

View File

@ -20,8 +20,8 @@ description: 学习如何实现完整的游戏存档系统,支持多槽位和
- 已安装 GFramework.Game NuGet 包
- 了解 C# 基础语法和 async/await
- 阅读过[快速开始](/zh-CN/getting-started/quick-start.md)
- 了解[数据与存档系统](/zh-CN/game/data.md)
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[数据与存档系统](/zh-CN/game/data)
## 步骤 1定义存档数据结构
@ -954,13 +954,13 @@ MyGame/
恭喜!你已经实现了一个完整的存档系统。接下来可以学习:
- [Godot 完整项目搭建](/zh-CN/tutorials/godot-complete-project.md) - 在 Godot 中使用存档系统
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial.md) - 异步加载存档
- [数据与存档系统](/zh-CN/game/data.md) - 数据系统详细说明
- [Godot 完整项目搭建](/zh-CN/tutorials/godot-complete-project) - 在 Godot 中使用存档系统
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 异步加载存档
- [数据与存档系统](/zh-CN/game/data) - 数据系统详细说明
## 相关文档
- [数据与存档系统](/zh-CN/game/data.md) - 数据系统详细说明
- [对象池系统](/zh-CN/core/pool.md) - 结合对象池复用资源
- [协程系统](/zh-CN/core/coroutine.md) - 异步加载资源
- [System 层](/zh-CN/core/system.md) - System 详细说明
- [数据与存档系统](/zh-CN/game/data) - 数据系统详细说明
- [对象池系统](/zh-CN/core/pool) - 结合对象池复用资源
- [协程系统](/zh-CN/core/coroutine) - 异步加载资源
- [System 层](/zh-CN/core/system) - System 详细说明

View File

@ -20,8 +20,8 @@ description: 学习如何使用状态机系统管理游戏状态和场景切换
- 已安装 GFramework.Core NuGet 包
- 了解 C# 基础语法和 async/await
- 阅读过[快速开始](/zh-CN/getting-started/quick-start.md)
- 了解[生命周期管理](/zh-CN/core/lifecycle.md)
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[生命周期管理](/zh-CN/core/lifecycle)
## 步骤 1定义游戏状态
@ -734,13 +734,13 @@ MyGame/
恭喜!你已经掌握了状态机系统的使用。接下来可以学习:
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial.md) - 在状态中使用协程
- [资源管理最佳实践](/zh-CN/tutorials/resource-management.md) - 在加载状态中管理资源
- [实现存档系统](/zh-CN/tutorials/save-system.md) - 保存和恢复游戏状态
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 在状态中使用协程
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 在加载状态中管理资源
- [实现存档系统](/zh-CN/tutorials/save-system) - 保存和恢复游戏状态
## 相关文档
- [状态机系统](/zh-CN/core/state-machine.md) - 状态机详细说明
- [生命周期管理](/zh-CN/core/lifecycle.md) - 组件生命周期
- [System 层](/zh-CN/core/system.md) - System 详细说明
- [架构组件](/zh-CN/core/architecture.md) - 架构基础
- [状态机系统](/zh-CN/core/state-machine) - 状态机详细说明
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
- [System 层](/zh-CN/core/system) - System 详细说明
- [架构组件](/zh-CN/core/architecture) - 架构基础

View File

@ -22,8 +22,8 @@ description: 学习如何为 GFramework 项目编写单元测试
- 已安装 .NET SDK 8.0 或更高版本
- 了解 C# 基础语法
- 熟悉 xUnit 或 NUnit 测试框架
- 阅读过[快速开始](/zh-CN/getting-started/quick-start.md)
- 了解[架构系统](/zh-CN/core/architecture.md)
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[架构系统](/zh-CN/core/architecture)
## 步骤 1创建测试项目
@ -1143,5 +1143,5 @@ public void CalculateBonus_Should_Return_Double(int input, int expected)
- [NUnit 官方文档](https://docs.nunit.org/)
- [Moq 快速入门](https://github.com/moq/moq4/wiki/Quickstart)
- [架构设计模式](/zh-CN/best-practices/architecture-patterns.md)
- [性能优化最佳实践](/zh-CN/best-practices/performance.md)
- [架构设计模式](/zh-CN/best-practices/architecture-patterns)
- [性能优化最佳实践](/zh-CN/best-practices/performance)