mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 05:08:58 +08:00
Compare commits
27 Commits
a1dbed3c8d
...
4b5a760643
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b5a760643 | ||
|
|
2fa19f89b6 | ||
|
|
1a62f337a6 | ||
|
|
71f36c7c20 | ||
|
|
cdc6d7e028 | ||
|
|
9295480866 | ||
|
|
7d6ff77ff4 | ||
|
|
5b7105d5be | ||
|
|
9656393fbb | ||
|
|
b8c2ad42a9 | ||
|
|
ba3a4d4a37 | ||
|
|
2fc8442bd4 | ||
|
|
050f4321c6 | ||
|
|
4ef9406ee9 | ||
|
|
df68cdfd82 | ||
|
|
12f15961af | ||
|
|
6d4f9f2f94 | ||
|
|
0f8bf077e4 | ||
|
|
78a23bf53a | ||
|
|
de782ae179 | ||
|
|
7ec2185ae0 | ||
|
|
97573be2e1 | ||
|
|
3f95843d59 | ||
|
|
df91d3706b | ||
|
|
737dd5d91d | ||
|
|
819f91a7ad | ||
|
|
007c33f772 |
@ -12,7 +12,9 @@ Shortcut: `$gframework-pr-review`
|
||||
## Workflow
|
||||
|
||||
1. Read `AGENTS.md` before deciding how to validate or fix anything.
|
||||
2. Resolve the current branch with Windows Git from WSL, following the repository worktree rule.
|
||||
2. Resolve the current branch following the repository worktree rule:
|
||||
- prefer Linux `git` with explicit `--git-dir` / `--work-tree` binding in WSL worktrees
|
||||
- only fall back to `git.exe` when that executable is available and actually runnable in the current session
|
||||
3. Run `scripts/fetch_current_pr_review.py` to:
|
||||
- locate the PR for the current branch through the GitHub PR API
|
||||
- fetch PR metadata, issue comments, reviews, and review comments through the GitHub API
|
||||
@ -31,20 +33,20 @@ Shortcut: `$gframework-pr-review`
|
||||
## Commands
|
||||
|
||||
- Default:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
|
||||
- Recommended machine-readable workflow:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 265 --json-output /tmp/pr265-review.json`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 265 --json-output /tmp/pr265-review.json`
|
||||
- `jq '.coderabbit_review.outside_diff_comments' /tmp/pr265-review.json`
|
||||
- Force a PR number:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253`
|
||||
- Machine-readable output:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
|
||||
- Write machine-readable output to a file instead of stdout:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --format json --json-output /tmp/pr253-review.json`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --format json --json-output /tmp/pr253-review.json`
|
||||
- Inspect only a high-signal section:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff`
|
||||
- Narrow text output to one path fragment:
|
||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff --path GFramework.Core/Events/Event.cs`
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff --path GFramework.Core/Events/Event.cs`
|
||||
|
||||
## Output Expectations
|
||||
|
||||
@ -67,6 +69,7 @@ The script should produce:
|
||||
|
||||
- If the current branch has no matching public PR, report that clearly instead of guessing.
|
||||
- If GitHub access fails because of proxy configuration, rerun the fetch with proxy variables removed.
|
||||
- If the current WSL session resolves `git.exe` but cannot execute it cleanly, keep using the explicit Linux worktree binding instead of retrying Windows Git.
|
||||
- Prefer GitHub API results over PR HTML. The PR HTML page is now a fallback/debugging source, not the primary source of truth.
|
||||
- If the summary block and the latest head review threads disagree, trust the latest unresolved head-review threads and treat older summary findings as stale until re-verified locally.
|
||||
- Do not assume every AI reviewer behaves like CodeRabbit. `greptile-apps[bot]` findings may exist only as latest-head review threads, without CodeRabbit-style issue comments or folded review-body sections.
|
||||
|
||||
@ -21,8 +21,11 @@ from typing import Any
|
||||
|
||||
OWNER = "GeWuYou"
|
||||
REPO = "GFramework"
|
||||
WORKTREE_ROOT_DIRECTORY_NAME = "GFramework-WorkTree"
|
||||
DEFAULT_WINDOWS_GIT = "/mnt/d/Tool/Development Tools/Git/cmd/git.exe"
|
||||
GIT_ENVIRONMENT_KEY = "GFRAMEWORK_WINDOWS_GIT"
|
||||
GIT_DIR_ENVIRONMENT_KEY = "GFRAMEWORK_GIT_DIR"
|
||||
WORK_TREE_ENVIRONMENT_KEY = "GFRAMEWORK_WORK_TREE"
|
||||
USER_AGENT = "codex-gframework-pr-review"
|
||||
CODERABBIT_LOGIN = "coderabbitai[bot]"
|
||||
GREPTILE_LOGIN = "greptile-apps[bot]"
|
||||
@ -83,6 +86,43 @@ def resolve_git_command() -> str:
|
||||
raise RuntimeError(f"No usable git executable found. Set {GIT_ENVIRONMENT_KEY} to override it.")
|
||||
|
||||
|
||||
def find_repository_root(start_path: Path) -> Path | None:
|
||||
"""Locate the repository root by walking parent directories for repo markers."""
|
||||
for candidate in (start_path, *start_path.parents):
|
||||
if (candidate / "AGENTS.md").exists() and (candidate / ".ai/environment/tools.ai.yaml").exists():
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_worktree_git_dir(repository_root: Path) -> Path | None:
|
||||
"""Resolve the main-repository worktree gitdir for this WSL worktree layout."""
|
||||
if repository_root.parent.name != WORKTREE_ROOT_DIRECTORY_NAME:
|
||||
return None
|
||||
|
||||
primary_repository_root = repository_root.parent.parent / REPO
|
||||
candidate_git_dir = primary_repository_root / ".git" / "worktrees" / repository_root.name
|
||||
return candidate_git_dir if candidate_git_dir.exists() else None
|
||||
|
||||
|
||||
def resolve_git_invocation() -> list[str]:
|
||||
"""Resolve the git command arguments, preferring explicit WSL worktree binding."""
|
||||
configured_git_dir = os.environ.get(GIT_DIR_ENVIRONMENT_KEY)
|
||||
configured_work_tree = os.environ.get(WORK_TREE_ENVIRONMENT_KEY)
|
||||
linux_git = shutil.which("git")
|
||||
|
||||
if configured_git_dir and configured_work_tree and linux_git:
|
||||
return [linux_git, f"--git-dir={configured_git_dir}", f"--work-tree={configured_work_tree}"]
|
||||
|
||||
repository_root = find_repository_root(Path.cwd())
|
||||
if repository_root is not None and linux_git:
|
||||
worktree_git_dir = resolve_worktree_git_dir(repository_root)
|
||||
if worktree_git_dir is not None:
|
||||
return [linux_git, f"--git-dir={worktree_git_dir}", f"--work-tree={repository_root}"]
|
||||
|
||||
return [resolve_git_command()]
|
||||
|
||||
|
||||
def resolve_request_timeout_seconds() -> int:
|
||||
"""Return the GitHub request timeout in seconds."""
|
||||
configured_timeout = os.environ.get(REQUEST_TIMEOUT_ENVIRONMENT_KEY)
|
||||
@ -113,7 +153,7 @@ def run_command(args: list[str]) -> str:
|
||||
|
||||
def get_current_branch() -> str:
|
||||
"""Return the current git branch name."""
|
||||
return run_command([resolve_git_command(), "rev-parse", "--abbrev-ref", "HEAD"])
|
||||
return run_command([*resolve_git_invocation(), "rev-parse", "--abbrev-ref", "HEAD"])
|
||||
|
||||
|
||||
def open_url(url: str, accept: str) -> tuple[str, Any]:
|
||||
|
||||
29
AGENTS.md
29
AGENTS.md
@ -10,20 +10,33 @@ All AI agents and contributors must follow these rules when writing, reviewing,
|
||||
- Use `@.ai/environment/tools.raw.yaml` only when you need the full collected facts behind the AI-facing hints.
|
||||
- Prefer the project-relevant tools listed there instead of assuming every installed system tool is fair game.
|
||||
- If the real environment differs from the inventory, use the project-relevant installed tool and report the mismatch.
|
||||
- When working in WSL against this repository's Windows-backed worktree, prefer Windows Git from WSL (for example
|
||||
`git.exe`) instead of the Linux `git` binary.
|
||||
- If a Git command in WSL fails with a worktree-style “not a git repository” path translation error, rerun it with the
|
||||
Windows Git executable and treat that as the repository-default Git path for the rest of the task.
|
||||
- If the shell does not currently resolve `git.exe` to the host Windows Git installation, prepend that installation's
|
||||
command directory to `PATH` and reset shell command hashing for the current session before continuing.
|
||||
- After resolving the host Windows Git path, prefer an explicit session-local binding for subsequent commands so the
|
||||
shell does not fall back to Linux `/usr/bin/git` later in the same WSL session.
|
||||
- When working in WSL against this repository's Windows-backed worktree, first prefer Linux `git` with an explicit
|
||||
`--git-dir=<repo>/.git/worktrees/<worktree-name>` and `--work-tree=<worktree-root>` binding for every repository
|
||||
command. Treat that explicit binding as higher priority than `git.exe`, because it avoids WSL worktree path
|
||||
translation mistakes and still works in sessions where Windows `.exe` execution is unavailable.
|
||||
- If a plain Linux `git` command in WSL fails with a worktree-style “not a git repository” path translation error,
|
||||
rerun it with the explicit `--git-dir` / `--work-tree` binding before trying `git.exe`.
|
||||
- Only prefer Windows Git from WSL (for example `git.exe`) when that executable is both resolvable and executable in the
|
||||
current session, and when the explicit Linux `git` binding is unavailable or has already failed.
|
||||
- If the shell resolves `git.exe` but the current WSL session cannot execute it cleanly (for example `Exec format
|
||||
error`), keep using the explicit Linux `git` binding for the rest of the task instead of retrying Windows Git.
|
||||
- If the shell does not currently resolve `git.exe` to the host Windows Git installation and you still need Windows Git
|
||||
as a fallback, prepend that installation's command directory to `PATH` and reset shell command hashing for the
|
||||
current session before continuing.
|
||||
- After resolving either strategy, prefer a session-local binding or command wrapper for subsequent Git commands so the
|
||||
shell does not silently fall back to the wrong repository context later in the same WSL session.
|
||||
|
||||
## Git Workflow Rules
|
||||
|
||||
- Every completed task MUST pass at least one build validation before it is considered done.
|
||||
- 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
|
||||
module/project instead of relying on an unrelated project or solution slice that does not actually compile the touched
|
||||
code.
|
||||
- Warnings reported by those affected-module builds are part of the task scope. Contributors MUST resolve the touched
|
||||
module's build warnings in the same change, or stop and explicitly report the exact warning IDs and blocker instead of
|
||||
deferring them to a separate long-lived cleanup branch by default.
|
||||
- If the required build passes and there are task-related staged or unstaged changes, contributors MUST create a Git
|
||||
commit automatically instead of leaving the task uncommitted, unless the user explicitly says not to commit.
|
||||
- Commit messages MUST use Conventional Commits format: `<type>(<scope>): <summary>`.
|
||||
|
||||
@ -96,6 +96,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
|
||||
var interfaceName = iContextAware.ToDisplayString(
|
||||
SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var memberNames = CreateGeneratedContextMemberNames(symbol);
|
||||
sb.AppendLine("/// <summary>");
|
||||
sb.AppendLine("/// 为当前规则类型补充自动生成的架构上下文访问实现。");
|
||||
sb.AppendLine("/// </summary>");
|
||||
@ -107,15 +108,15 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
sb.AppendLine(
|
||||
"/// 已缓存的实例上下文需要通过 <see cref=\"GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)\" /> 显式覆盖。");
|
||||
sb.AppendLine(
|
||||
"/// 与手动继承 <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 的路径相比,生成实现会使用 <c>_contextSync</c> 协调惰性初始化、provider 切换和显式上下文注入;");
|
||||
$"/// 与手动继承 <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 的路径相比,生成实现会使用 <c>{memberNames.SyncFieldName}</c> 协调惰性初始化、provider 切换和显式上下文注入;");
|
||||
sb.AppendLine(
|
||||
"/// <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。");
|
||||
sb.AppendLine("/// </remarks>");
|
||||
sb.AppendLine($"partial class {symbol.Name} : {interfaceName}");
|
||||
sb.AppendLine("{");
|
||||
|
||||
GenerateContextProperty(sb);
|
||||
GenerateInterfaceImplementations(sb, iContextAware);
|
||||
GenerateContextProperty(sb, memberNames);
|
||||
GenerateInterfaceImplementations(sb, iContextAware, memberNames);
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString().TrimEnd();
|
||||
@ -138,13 +139,40 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
/// 生成Context属性
|
||||
/// </summary>
|
||||
/// <param name="sb">字符串构建器</param>
|
||||
private static void GenerateContextProperty(StringBuilder sb)
|
||||
/// <param name="memberNames">当前目标类型应使用的上下文字段名。</param>
|
||||
private static void GenerateContextProperty(
|
||||
StringBuilder sb,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
GenerateContextBackingFields(sb, memberNames);
|
||||
GenerateContextGetter(sb, memberNames);
|
||||
GenerateContextProviderConfiguration(sb, memberNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成上下文缓存和同步所需的字段。
|
||||
/// </summary>
|
||||
/// <param name="sb">字符串构建器。</param>
|
||||
private static void GenerateContextBackingFields(
|
||||
StringBuilder sb,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
sb.AppendLine(" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;");
|
||||
sb.AppendLine(
|
||||
" private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider;");
|
||||
sb.AppendLine(" private static readonly object _contextSync = new();");
|
||||
$" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? {memberNames.ContextFieldName};");
|
||||
sb.AppendLine(
|
||||
$" private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? {memberNames.ProviderFieldName};");
|
||||
sb.AppendLine($" private static readonly object {memberNames.SyncFieldName} = new();");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成实例上下文访问器,包含显式注入优先和 provider 惰性回退语义。
|
||||
/// </summary>
|
||||
/// <param name="sb">字符串构建器。</param>
|
||||
private static void GenerateContextGetter(
|
||||
StringBuilder sb,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// 获取当前实例绑定的架构上下文。");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
@ -158,7 +186,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
sb.AppendLine(
|
||||
" /// 或 <see cref=\"ResetContextProvider\" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。");
|
||||
sb.AppendLine(
|
||||
" /// 当前实现还假设 <see cref=\"GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext\" /> 可在持有 <c>_contextSync</c> 时安全执行;");
|
||||
$" /// 当前实现还假设 <see cref=\"GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext\" /> 可在持有 <c>{memberNames.SyncFieldName}</c> 时安全执行;");
|
||||
sb.AppendLine(
|
||||
" /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。");
|
||||
sb.AppendLine(" /// </remarks>");
|
||||
@ -166,29 +194,35 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" get");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" var context = _context;");
|
||||
sb.AppendLine(" if (context is not null)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" return context;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。");
|
||||
sb.AppendLine(
|
||||
" // provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。");
|
||||
sb.AppendLine(" lock (_contextSync)");
|
||||
$" // provider 的 GetContext() 会在持有 {memberNames.SyncFieldName} 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。");
|
||||
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(
|
||||
" _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();");
|
||||
sb.AppendLine(" _context ??= _contextProvider.GetContext();");
|
||||
sb.AppendLine(" return _context;");
|
||||
$" {memberNames.ProviderFieldName} ??= new global::GFramework.Core.Architectures.GameContextProvider();");
|
||||
sb.AppendLine($" {memberNames.ContextFieldName} ??= {memberNames.ProviderFieldName}.GetContext();");
|
||||
sb.AppendLine($" return {memberNames.ContextFieldName};");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成静态 provider 配置 API,供测试和宿主在懒加载前替换默认上下文来源。
|
||||
/// </summary>
|
||||
/// <param name="sb">字符串构建器。</param>
|
||||
private static void GenerateContextProviderConfiguration(
|
||||
StringBuilder sb,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// 配置当前生成类型共享的上下文提供者。");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" /// <param name=\"provider\">后续懒加载上下文时要使用的提供者实例。</param>");
|
||||
sb.AppendLine(
|
||||
" /// <exception cref=\"global::System.ArgumentNullException\">当 <paramref name=\"provider\" /> 为 null 时抛出。</exception>");
|
||||
sb.AppendLine(" /// <remarks>");
|
||||
sb.AppendLine(" /// 该方法使用与 <see cref=\"Context\" /> 相同的同步锁,避免提供者切换与惰性初始化交错。");
|
||||
sb.AppendLine(
|
||||
@ -198,9 +232,10 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
sb.AppendLine(
|
||||
" public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" lock (_contextSync)");
|
||||
sb.AppendLine(" global::System.ArgumentNullException.ThrowIfNull(provider);");
|
||||
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _contextProvider = provider;");
|
||||
sb.AppendLine($" {memberNames.ProviderFieldName} = provider;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
@ -215,9 +250,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
sb.AppendLine(" /// </remarks>");
|
||||
sb.AppendLine(" public static void ResetContextProvider()");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" lock (_contextSync)");
|
||||
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _contextProvider = null;");
|
||||
sb.AppendLine($" {memberNames.ProviderFieldName} = null;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
@ -234,7 +269,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
/// <param name="interfaceSymbol">接口符号</param>
|
||||
private static void GenerateInterfaceImplementations(
|
||||
StringBuilder sb,
|
||||
INamedTypeSymbol interfaceSymbol)
|
||||
INamedTypeSymbol interfaceSymbol,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
var interfaceName = interfaceSymbol.ToDisplayString(
|
||||
SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
@ -244,7 +280,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
if (method.MethodKind != MethodKind.Ordinary)
|
||||
continue;
|
||||
|
||||
GenerateMethod(sb, interfaceName, method);
|
||||
GenerateMethod(sb, interfaceName, method, memberNames);
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
@ -258,7 +294,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
private static void GenerateMethod(
|
||||
StringBuilder sb,
|
||||
string interfaceName,
|
||||
IMethodSymbol method)
|
||||
IMethodSymbol method,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
var returnType = method.ReturnType.ToDisplayString(
|
||||
SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
@ -271,7 +308,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
$" {returnType} {interfaceName}.{method.Name}({parameters})");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
GenerateMethodBody(sb, method);
|
||||
GenerateMethodBody(sb, method, memberNames);
|
||||
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
@ -283,15 +320,16 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
/// <param name="method">方法符号</param>
|
||||
private static void GenerateMethodBody(
|
||||
StringBuilder sb,
|
||||
IMethodSymbol method)
|
||||
IMethodSymbol method,
|
||||
GeneratedContextMemberNames memberNames)
|
||||
{
|
||||
switch (method.Name)
|
||||
{
|
||||
case "SetContext":
|
||||
sb.AppendLine(" // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。");
|
||||
sb.AppendLine(" lock (_contextSync)");
|
||||
sb.AppendLine($" lock ({memberNames.SyncFieldName})");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" _context = context;");
|
||||
sb.AppendLine($" {memberNames.ContextFieldName} = context;");
|
||||
sb.AppendLine(" }");
|
||||
break;
|
||||
|
||||
@ -307,4 +345,75 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为生成字段选择不会与目标类型现有成员冲突的稳定名称。
|
||||
/// </summary>
|
||||
/// <param name="symbol">当前需要补充 ContextAware 实现的目标类型。</param>
|
||||
/// <returns>当前生成轮次应使用的上下文字段名集合。</returns>
|
||||
private static GeneratedContextMemberNames CreateGeneratedContextMemberNames(INamedTypeSymbol symbol)
|
||||
{
|
||||
var reservedNames = CollectReservedContextMemberNames(symbol);
|
||||
|
||||
return new GeneratedContextMemberNames(
|
||||
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareContext"),
|
||||
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareProvider"),
|
||||
AllocateGeneratedMemberName(reservedNames, "_gFrameworkContextAwareSync"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收集当前类型及其基类链上所有显式声明的成员名,确保生成字段不会意外隐藏继承成员。
|
||||
/// </summary>
|
||||
/// <param name="symbol">当前需要补充 ContextAware 实现的目标类型。</param>
|
||||
/// <returns>已被当前类型层级占用的成员名集合。</returns>
|
||||
private static HashSet<string> CollectReservedContextMemberNames(INamedTypeSymbol symbol)
|
||||
{
|
||||
var reservedNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
// Walk the full inheritance chain so numeric suffix allocation also covers members introduced by base types.
|
||||
for (var currentType = symbol; currentType is not null; currentType = currentType.BaseType)
|
||||
{
|
||||
foreach (var member in currentType.GetMembers())
|
||||
{
|
||||
if (!member.IsImplicitlyDeclared)
|
||||
{
|
||||
reservedNames.Add(member.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reservedNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在固定前缀基础上按顺序追加数字后缀,直到找到可安全写入的成员名。
|
||||
/// </summary>
|
||||
/// <param name="reservedNames">当前类型已占用或已为其他生成字段保留的名称集合。</param>
|
||||
/// <param name="baseName">优先尝试的基础名称。</param>
|
||||
/// <returns>本轮生成可以使用的唯一成员名。</returns>
|
||||
private static string AllocateGeneratedMemberName(
|
||||
ISet<string> reservedNames,
|
||||
string baseName)
|
||||
{
|
||||
if (reservedNames.Add(baseName))
|
||||
return baseName;
|
||||
|
||||
for (var suffix = 1; ; suffix++)
|
||||
{
|
||||
var candidateName = $"{baseName}{suffix}";
|
||||
if (reservedNames.Add(candidateName))
|
||||
return candidateName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述一次 ContextAware 代码生成中选定的上下文字段名。
|
||||
/// </summary>
|
||||
/// <param name="ContextFieldName">实例上下文缓存字段名。</param>
|
||||
/// <param name="ProviderFieldName">共享上下文提供者字段名。</param>
|
||||
/// <param name="SyncFieldName">用于串行化访问的同步字段名。</param>
|
||||
private readonly record struct GeneratedContextMemberNames(
|
||||
string ContextFieldName,
|
||||
string ProviderFieldName,
|
||||
string SyncFieldName);
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ public class EasyEventsTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试并发场景下AddEvent的行为
|
||||
/// 测试 AddEvent 对重复事件类型保持兼容的参数异常类型。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void AddEvent_Should_Throw_When_Already_Registered()
|
||||
@ -167,4 +167,4 @@ public class EasyEventsTests
|
||||
Assert.That(_easyEvents.GetEvent<Event<int, string>>(), Is.Not.Null);
|
||||
Assert.That(_easyEvents.GetEvent<Event<double>>(), Is.Not.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,6 +165,30 @@ public class CollectionExtensionsTests
|
||||
Assert.That(result["c"], Is.EqualTo(3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试ToDictionarySafe保持具体Dictionary返回类型,避免公开API继续收窄。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ToDictionarySafe_Should_Preserve_Concrete_Return_Type()
|
||||
{
|
||||
var method = typeof(GFramework.Core.Extensions.CollectionExtensions)
|
||||
.GetMethods()
|
||||
.Single(static method => method.Name == nameof(GFramework.Core.Extensions.CollectionExtensions.ToDictionarySafe));
|
||||
var methodGenericArguments = method.GetGenericArguments();
|
||||
var returnTypeGenericArguments = method.ReturnType.GetGenericArguments();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(method.IsGenericMethodDefinition, Is.True);
|
||||
Assert.That(method.ReturnType.IsGenericType, Is.True);
|
||||
Assert.That(method.ReturnType.GetGenericTypeDefinition(), Is.EqualTo(typeof(Dictionary<,>)));
|
||||
Assert.That(methodGenericArguments.Select(static argument => argument.Name), Is.EqualTo(new[] { "T", "TKey", "TValue" }));
|
||||
Assert.That(returnTypeGenericArguments, Has.Length.EqualTo(2));
|
||||
Assert.That(returnTypeGenericArguments[0], Is.SameAs(methodGenericArguments[1]));
|
||||
Assert.That(returnTypeGenericArguments[1], Is.SameAs(methodGenericArguments[2]));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试ToDictionarySafe方法在存在重复键时覆盖前面的值
|
||||
/// </summary>
|
||||
@ -224,4 +248,4 @@ public class CollectionExtensionsTests
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
items.ToDictionarySafe<(string, int), string, int>(x => x.Item1, null!));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,6 +39,40 @@ public class LoggingConfigurationTests
|
||||
Assert.That(config.LoggerLevels["GFramework.Core"], Is.EqualTo(LogLevel.Trace));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Configuration_Collections_Should_Preserve_Public_Concrete_Types()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
typeof(LoggingConfiguration).GetProperty(nameof(LoggingConfiguration.Appenders))!.PropertyType,
|
||||
Is.EqualTo(typeof(List<AppenderConfiguration>)));
|
||||
Assert.That(
|
||||
typeof(LoggingConfiguration).GetProperty(nameof(LoggingConfiguration.LoggerLevels))!.PropertyType,
|
||||
Is.EqualTo(typeof(Dictionary<string, LogLevel>)));
|
||||
Assert.That(
|
||||
typeof(FilterConfiguration).GetProperty(nameof(FilterConfiguration.Namespaces))!.PropertyType,
|
||||
Is.EqualTo(typeof(List<string>)));
|
||||
Assert.That(
|
||||
typeof(FilterConfiguration).GetProperty(nameof(FilterConfiguration.Filters))!.PropertyType,
|
||||
Is.EqualTo(typeof(List<FilterConfiguration>)));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LoggerLevels_Should_Remain_Case_Sensitive_By_Default()
|
||||
{
|
||||
var config = new LoggingConfiguration();
|
||||
config.LoggerLevels["GFramework.Core"] = LogLevel.Info;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(config.LoggerLevels.ContainsKey("GFramework.Core"), Is.True);
|
||||
Assert.That(config.LoggerLevels["GFramework.Core"], Is.EqualTo(LogLevel.Info));
|
||||
Assert.That(config.LoggerLevels.ContainsKey("gframework.core"), Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LoadFromJsonString_WithInvalidJson_ShouldThrow()
|
||||
{
|
||||
|
||||
@ -41,7 +41,7 @@ public sealed class CoroutineScheduler(
|
||||
|
||||
private readonly Dictionary<CoroutineHandle, CoroutineCompletionStatus> _completionStatuses = new();
|
||||
private readonly Queue<CoroutineHandle> _completionStatusOrder = new();
|
||||
private readonly Dictionary<string, HashSet<CoroutineHandle>> _grouped = new();
|
||||
private readonly Dictionary<string, HashSet<CoroutineHandle>> _grouped = new(StringComparer.Ordinal);
|
||||
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler));
|
||||
private readonly Dictionary<CoroutineHandle, CoroutineMetadata> _metadata = new();
|
||||
private readonly ConcurrentQueue<CoroutineHandle> _pendingKills = new();
|
||||
@ -50,7 +50,7 @@ public sealed class CoroutineScheduler(
|
||||
throw new ArgumentNullException(nameof(timeSource));
|
||||
|
||||
private readonly CoroutineStatistics? _statistics = enableStatistics ? new CoroutineStatistics() : null;
|
||||
private readonly Dictionary<string, HashSet<CoroutineHandle>> _tagged = new();
|
||||
private readonly Dictionary<string, HashSet<CoroutineHandle>> _tagged = new(StringComparer.Ordinal);
|
||||
private readonly ITimeSource _timeSource = timeSource ?? throw new ArgumentNullException(nameof(timeSource));
|
||||
private readonly Dictionary<CoroutineHandle, HashSet<CoroutineHandle>> _waiting = new();
|
||||
private int _nextSlot;
|
||||
|
||||
@ -53,12 +53,14 @@ public class EasyEvents
|
||||
/// 添加指定类型的事件到事件字典中
|
||||
/// </summary>
|
||||
/// <typeparam name="T">事件类型,必须实现IEasyEvent接口且具有无参构造函数</typeparam>
|
||||
/// <exception cref="ArgumentException">当事件类型已存在时抛出</exception>
|
||||
/// <exception cref="ArgumentException">当事件类型已存在时抛出。</exception>
|
||||
public void AddEvent<T>() where T : IEvent, new()
|
||||
{
|
||||
if (!_mTypeEvents.TryAdd(typeof(T), new T()))
|
||||
{
|
||||
#pragma warning disable MA0015 // Preserve the public ArgumentException contract without inventing a fake parameter name.
|
||||
throw new ArgumentException($"Event type {typeof(T).Name} already registered.");
|
||||
#pragma warning restore MA0015
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,4 +83,4 @@ public class EasyEvents
|
||||
{
|
||||
return (T)_mTypeEvents.GetOrAdd(typeof(T), _ => new T());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,10 +81,12 @@ public static class CollectionExtensions
|
||||
/// // dict["a"] == 3 (最后一个值)
|
||||
/// </code>
|
||||
/// </example>
|
||||
#pragma warning disable MA0016 // Preserve the established concrete return type for public API compatibility.
|
||||
public static Dictionary<TKey, TValue> ToDictionarySafe<T, TKey, TValue>(
|
||||
this IEnumerable<T> source,
|
||||
Func<T, TKey> keySelector,
|
||||
Func<T, TValue> valueSelector) where TKey : notnull
|
||||
#pragma warning restore MA0016
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(keySelector);
|
||||
|
||||
@ -16,8 +16,23 @@ namespace GFramework.Core.Functional;
|
||||
/// <summary>
|
||||
/// 表示可能存在或不存在的值,用于替代 null 引用的函数式编程类型
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="Option{T}" /> 只表示两种显式状态:通过 <see cref="Some(T)" /> 创建的有值状态,以及
|
||||
/// <see cref="None" /> 表示的无值状态;调用方不应把 <see cref="None" /> 当作 <see langword="null" /> 的别名。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <see cref="Some(T)" /> 会拒绝 <see langword="null" />,因此引用类型和可空引用类型参数都必须包装真实值;访问方应优先通过
|
||||
/// <see cref="IsSome" />、<see cref="IsNone" />、模式匹配或 <c>Match</c>/<c>Map</c> 等函数式 API 消费结果,而不是假设默认值
|
||||
/// 与无值状态等价。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 该结构体是不可变值类型;一旦创建,其状态与内部值不会再改变。但在 <see cref="IsNone" /> 为 <see langword="true" /> 时,
|
||||
/// 调用需要真实值的方法仍应遵守各成员自身的契约与异常说明。
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">值的类型</typeparam>
|
||||
public readonly struct Option<T>
|
||||
public readonly struct Option<T> : IEquatable<Option<T>>
|
||||
{
|
||||
private readonly T _value;
|
||||
private readonly bool _isSome;
|
||||
@ -313,4 +328,4 @@ public readonly struct Option<T>
|
||||
_isSome ? $"Some({_value})" : "None";
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,10 +20,14 @@ public sealed class FilterConfiguration
|
||||
/// <summary>
|
||||
/// 命名空间前缀列表(用于 Namespace 过滤器)。
|
||||
/// </summary>
|
||||
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
|
||||
public List<string>? Namespaces { get; set; }
|
||||
#pragma warning restore MA0016
|
||||
|
||||
/// <summary>
|
||||
/// 子过滤器列表(用于 Composite 过滤器)。
|
||||
/// </summary>
|
||||
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
|
||||
public List<FilterConfiguration>? Filters { get; set; }
|
||||
#pragma warning restore MA0016
|
||||
}
|
||||
|
||||
@ -15,10 +15,15 @@ public sealed class LoggingConfiguration
|
||||
/// <summary>
|
||||
/// Appender 配置列表
|
||||
/// </summary>
|
||||
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
|
||||
public List<AppenderConfiguration> Appenders { get; set; } = new();
|
||||
#pragma warning restore MA0016
|
||||
|
||||
/// <summary>
|
||||
/// 特定 Logger 的日志级别配置
|
||||
/// </summary>
|
||||
public Dictionary<string, LogLevel> LoggerLevels { get; set; } = new(StringComparer.Ordinal);
|
||||
#pragma warning disable MA0016 // Preserve the established concrete configuration API surface.
|
||||
public Dictionary<string, LogLevel> LoggerLevels { get; set; } =
|
||||
new Dictionary<string, LogLevel>(StringComparer.Ordinal);
|
||||
#pragma warning restore MA0016
|
||||
}
|
||||
|
||||
@ -0,0 +1,284 @@
|
||||
namespace GFramework.Cqrs.SourceGenerators.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
|
||||
/// </summary>
|
||||
public sealed partial class CqrsHandlerRegistryGenerator
|
||||
{
|
||||
private readonly record struct HandlerRegistrationSpec(
|
||||
string HandlerInterfaceDisplayName,
|
||||
string ImplementationTypeDisplayName,
|
||||
string HandlerInterfaceLogName,
|
||||
string ImplementationLogName);
|
||||
|
||||
private readonly record struct ReflectedImplementationRegistrationSpec(
|
||||
string HandlerInterfaceDisplayName,
|
||||
string HandlerInterfaceLogName);
|
||||
|
||||
private readonly record struct OrderedRegistrationSpec(
|
||||
string HandlerInterfaceLogName,
|
||||
OrderedRegistrationKind Kind,
|
||||
int Index);
|
||||
|
||||
private readonly record struct GeneratedRegistrySourceShape(
|
||||
bool HasReflectedImplementationRegistrations,
|
||||
bool HasPreciseReflectedRegistrations,
|
||||
bool HasReflectionTypeLookups,
|
||||
bool HasExternalAssemblyTypeLookups)
|
||||
{
|
||||
public bool RequiresRegistryAssemblyVariable =>
|
||||
HasReflectedImplementationRegistrations ||
|
||||
HasPreciseReflectedRegistrations ||
|
||||
HasReflectionTypeLookups;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该枚举只服务于输出排序与代码分支选择,用来保证生成注册器在“直接注册”
|
||||
/// “反射实现类型查找”和“精确运行时类型解析”之间保持稳定顺序。
|
||||
/// </remarks>
|
||||
private enum OrderedRegistrationKind
|
||||
{
|
||||
Direct,
|
||||
ReflectedImplementation,
|
||||
PreciseReflected
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述生成注册器中某个运行时类型引用的构造方式。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 某些 handler 服务类型可以直接以 <c>typeof(...)</c> 输出,某些则需要在运行时补充
|
||||
/// 反射查找、数组封装或泛型实参重建。该记录把这些差异收敛为统一的递归结构,
|
||||
/// 供源码输出阶段生成稳定的类型解析语句。
|
||||
/// </remarks>
|
||||
private sealed record RuntimeTypeReferenceSpec(
|
||||
string? TypeDisplayName,
|
||||
string? ReflectionTypeMetadataName,
|
||||
string? ReflectionAssemblyName,
|
||||
RuntimeTypeReferenceSpec? ArrayElementTypeReference,
|
||||
int ArrayRank,
|
||||
RuntimeTypeReferenceSpec? PointerElementTypeReference,
|
||||
RuntimeTypeReferenceSpec? GenericTypeDefinitionReference,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> GenericTypeArguments)
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个可直接通过 <c>typeof(...)</c> 表达的类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromDirectReference(string typeDisplayName)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(
|
||||
typeDisplayName,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个需要从当前消费端程序集反射解析的类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromReflectionLookup(string reflectionTypeMetadataName)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(
|
||||
null,
|
||||
reflectionTypeMetadataName,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个需要从被引用程序集反射解析的类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromExternalReflectionLookup(
|
||||
string reflectionAssemblyName,
|
||||
string reflectionTypeMetadataName)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(
|
||||
null,
|
||||
reflectionTypeMetadataName,
|
||||
reflectionAssemblyName,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个数组类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromArray(RuntimeTypeReferenceSpec elementTypeReference, int arrayRank)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
elementTypeReference,
|
||||
arrayRank,
|
||||
null,
|
||||
null,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个封闭泛型类型引用。
|
||||
/// </summary>
|
||||
public static RuntimeTypeReferenceSpec FromConstructedGeneric(
|
||||
RuntimeTypeReferenceSpec genericTypeDefinitionReference,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> genericTypeArguments)
|
||||
{
|
||||
return new RuntimeTypeReferenceSpec(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
genericTypeDefinitionReference,
|
||||
genericTypeArguments);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct PreciseReflectedRegistrationSpec(
|
||||
string OpenHandlerTypeDisplayName,
|
||||
string HandlerInterfaceLogName,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> ServiceTypeArguments);
|
||||
|
||||
private readonly record struct ImplementationRegistrationSpec(
|
||||
string ImplementationTypeDisplayName,
|
||||
string ImplementationLogName,
|
||||
ImmutableArray<HandlerRegistrationSpec> DirectRegistrations,
|
||||
ImmutableArray<ReflectedImplementationRegistrationSpec> ReflectedImplementationRegistrations,
|
||||
ImmutableArray<PreciseReflectedRegistrationSpec> PreciseReflectedRegistrations,
|
||||
string? ReflectionTypeMetadataName,
|
||||
string? ReflectionFallbackHandlerTypeMetadataName);
|
||||
|
||||
private readonly struct HandlerCandidateAnalysis : IEquatable<HandlerCandidateAnalysis>
|
||||
{
|
||||
public HandlerCandidateAnalysis(
|
||||
string implementationTypeDisplayName,
|
||||
string implementationLogName,
|
||||
ImmutableArray<HandlerRegistrationSpec> registrations,
|
||||
ImmutableArray<ReflectedImplementationRegistrationSpec> reflectedImplementationRegistrations,
|
||||
ImmutableArray<PreciseReflectedRegistrationSpec> preciseReflectedRegistrations,
|
||||
string? reflectionTypeMetadataName,
|
||||
string? reflectionFallbackHandlerTypeMetadataName)
|
||||
{
|
||||
ImplementationTypeDisplayName = implementationTypeDisplayName;
|
||||
ImplementationLogName = implementationLogName;
|
||||
Registrations = registrations;
|
||||
ReflectedImplementationRegistrations = reflectedImplementationRegistrations;
|
||||
PreciseReflectedRegistrations = preciseReflectedRegistrations;
|
||||
ReflectionTypeMetadataName = reflectionTypeMetadataName;
|
||||
ReflectionFallbackHandlerTypeMetadataName = reflectionFallbackHandlerTypeMetadataName;
|
||||
}
|
||||
|
||||
public string ImplementationTypeDisplayName { get; }
|
||||
|
||||
public string ImplementationLogName { get; }
|
||||
|
||||
public ImmutableArray<HandlerRegistrationSpec> Registrations { get; }
|
||||
|
||||
public ImmutableArray<ReflectedImplementationRegistrationSpec> ReflectedImplementationRegistrations { get; }
|
||||
|
||||
public ImmutableArray<PreciseReflectedRegistrationSpec> PreciseReflectedRegistrations { get; }
|
||||
|
||||
public string? ReflectionTypeMetadataName { get; }
|
||||
|
||||
public string? ReflectionFallbackHandlerTypeMetadataName { get; }
|
||||
|
||||
public bool Equals(HandlerCandidateAnalysis other)
|
||||
{
|
||||
if (!string.Equals(ImplementationTypeDisplayName, other.ImplementationTypeDisplayName,
|
||||
StringComparison.Ordinal) ||
|
||||
!string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) ||
|
||||
!string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName,
|
||||
StringComparison.Ordinal) ||
|
||||
!string.Equals(
|
||||
ReflectionFallbackHandlerTypeMetadataName,
|
||||
other.ReflectionFallbackHandlerTypeMetadataName,
|
||||
StringComparison.Ordinal) ||
|
||||
Registrations.Length != other.Registrations.Length ||
|
||||
ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length ||
|
||||
PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < Registrations.Length; index++)
|
||||
{
|
||||
if (!Registrations[index].Equals(other.Registrations[index]))
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var index = 0; index < ReflectedImplementationRegistrations.Length; index++)
|
||||
{
|
||||
if (!ReflectedImplementationRegistrations[index].Equals(
|
||||
other.ReflectedImplementationRegistrations[index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index < PreciseReflectedRegistrations.Length; index++)
|
||||
{
|
||||
if (!PreciseReflectedRegistrations[index].Equals(other.PreciseReflectedRegistrations[index]))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is HandlerCandidateAnalysis other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hashCode = StringComparer.Ordinal.GetHashCode(ImplementationTypeDisplayName);
|
||||
hashCode = (hashCode * 397) ^ StringComparer.Ordinal.GetHashCode(ImplementationLogName);
|
||||
hashCode = (hashCode * 397) ^
|
||||
(ReflectionTypeMetadataName is null
|
||||
? 0
|
||||
: StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName));
|
||||
hashCode = (hashCode * 397) ^
|
||||
(ReflectionFallbackHandlerTypeMetadataName is null
|
||||
? 0
|
||||
: StringComparer.Ordinal.GetHashCode(ReflectionFallbackHandlerTypeMetadataName));
|
||||
foreach (var registration in Registrations)
|
||||
{
|
||||
hashCode = (hashCode * 397) ^ registration.GetHashCode();
|
||||
}
|
||||
|
||||
foreach (var reflectedImplementationRegistration in ReflectedImplementationRegistrations)
|
||||
{
|
||||
hashCode = (hashCode * 397) ^ reflectedImplementationRegistration.GetHashCode();
|
||||
}
|
||||
|
||||
foreach (var preciseReflectedRegistration in PreciseReflectedRegistrations)
|
||||
{
|
||||
hashCode = (hashCode * 397) ^ preciseReflectedRegistration.GetHashCode();
|
||||
}
|
||||
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct GenerationEnvironment(
|
||||
bool GenerationEnabled,
|
||||
bool SupportsReflectionFallbackAttribute);
|
||||
}
|
||||
@ -0,0 +1,327 @@
|
||||
namespace GFramework.Cqrs.SourceGenerators.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
|
||||
/// </summary>
|
||||
public sealed partial class CqrsHandlerRegistryGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 为无法直接在生成代码中书写的关闭处理器接口构造精确的运行时注册描述。
|
||||
/// </summary>
|
||||
/// <param name="compilation">
|
||||
/// 当前生成轮次对应的编译上下文,用于判断类型是否属于当前程序集,从而决定是生成直接类型引用还是延迟到运行时反射解析。
|
||||
/// </param>
|
||||
/// <param name="handlerInterface">
|
||||
/// 需要注册的关闭处理器接口。调用方应保证它来自受支持的 CQRS 处理器接口定义,并且其泛型参数顺序与运行时注册约定一致。
|
||||
/// </param>
|
||||
/// <param name="registration">
|
||||
/// 当方法返回 <see langword="true" /> 时,包含开放泛型处理器类型和每个运行时类型实参的精确描述;
|
||||
/// 当方法返回 <see langword="false" /> 时,为默认值,调用方应回退到基于实现类型的宽松反射发现路径。
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// 当接口上的所有运行时类型引用都能在生成阶段被稳定描述时返回 <see langword="true" />;
|
||||
/// 只要任一泛型实参无法安全编码到生成输出中,就返回 <see langword="false" />。
|
||||
/// </returns>
|
||||
private static bool TryCreatePreciseReflectedRegistration(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol handlerInterface,
|
||||
out PreciseReflectedRegistrationSpec registration)
|
||||
{
|
||||
var openHandlerTypeDisplayName = handlerInterface.OriginalDefinition
|
||||
.ConstructUnboundGenericType()
|
||||
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var typeArguments =
|
||||
ImmutableArray.CreateBuilder<RuntimeTypeReferenceSpec>(handlerInterface.TypeArguments.Length);
|
||||
foreach (var typeArgument in handlerInterface.TypeArguments)
|
||||
{
|
||||
if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var runtimeTypeReference))
|
||||
{
|
||||
registration = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
typeArguments.Add(runtimeTypeReference!);
|
||||
}
|
||||
|
||||
registration = new PreciseReflectedRegistrationSpec(
|
||||
openHandlerTypeDisplayName,
|
||||
GetLogDisplayName(handlerInterface),
|
||||
typeArguments.ToImmutable());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Roslyn 类型符号转换为生成注册器可消费的运行时类型引用描述。
|
||||
/// </summary>
|
||||
/// <param name="compilation">
|
||||
/// 当前编译上下文,用于区分可直接引用的外部可访问类型与必须通过当前程序集运行时反射查找的内部类型。
|
||||
/// </param>
|
||||
/// <param name="type">
|
||||
/// 需要转换的类型符号。该方法会递归处理数组元素类型和已构造泛型的类型实参,但不会为未绑定泛型或类型参数生成引用。
|
||||
/// </param>
|
||||
/// <param name="runtimeTypeReference">
|
||||
/// 当方法返回 <see langword="true" /> 时,包含可直接引用、数组、已构造泛型或反射查找中的一种运行时表示;
|
||||
/// 当方法返回 <see langword="false" /> 时为 <see langword="null" />,调用方应回退到更宽泛的实现类型反射扫描策略。
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// 当 <paramref name="type" /> 及其递归子结构都能映射为稳定的运行时引用时返回 <see langword="true" />;
|
||||
/// 若遇到类型参数、无法访问的运行时结构,或任一递归分支无法表示,则返回 <see langword="false" />。
|
||||
/// </returns>
|
||||
private static bool TryCreateRuntimeTypeReference(
|
||||
Compilation compilation,
|
||||
ITypeSymbol type,
|
||||
out RuntimeTypeReferenceSpec? runtimeTypeReference)
|
||||
{
|
||||
// CLR forbids pointer and function-pointer types from being used as generic arguments.
|
||||
// CQRS handler contracts are generic interfaces, so emitting runtime reconstruction code for these
|
||||
// shapes would only defer the failure to MakeGenericType(...) at runtime.
|
||||
if (type is IPointerTypeSymbol or IFunctionPointerTypeSymbol)
|
||||
{
|
||||
runtimeTypeReference = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Roslyn models dynamic as a pseudo-type, but generated C# cannot emit typeof(dynamic).
|
||||
// Normalize it to the CLR runtime type so precise reflected registrations stay compilable.
|
||||
if (type.TypeKind == TypeKind.Dynamic)
|
||||
{
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference("global::System.Object");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CanReferenceFromGeneratedRegistry(compilation, type))
|
||||
{
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromDirectReference(
|
||||
type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type is IArrayTypeSymbol arrayType &&
|
||||
TryCreateRuntimeTypeReference(compilation, arrayType.ElementType, out var elementTypeReference))
|
||||
{
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromArray(elementTypeReference!, arrayType.Rank);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type is INamedTypeSymbol genericNamedType &&
|
||||
genericNamedType.IsGenericType &&
|
||||
!genericNamedType.IsUnboundGenericType)
|
||||
{
|
||||
return TryCreateConstructedGenericRuntimeTypeReference(
|
||||
compilation,
|
||||
genericNamedType,
|
||||
out runtimeTypeReference);
|
||||
}
|
||||
|
||||
if (type is INamedTypeSymbol namedType &&
|
||||
TryCreateNamedRuntimeTypeReference(compilation, namedType, out var namedTypeReference))
|
||||
{
|
||||
runtimeTypeReference = namedTypeReference;
|
||||
return true;
|
||||
}
|
||||
|
||||
runtimeTypeReference = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为已构造泛型类型构造运行时类型引用,并递归验证每个泛型实参都可以稳定编码到生成输出中。
|
||||
/// </summary>
|
||||
/// <param name="compilation">当前生成轮次的编译上下文。</param>
|
||||
/// <param name="genericNamedType">需要表示的已构造泛型类型。</param>
|
||||
/// <param name="runtimeTypeReference">
|
||||
/// 当方法返回 <see langword="true" /> 时,包含泛型定义和泛型实参的运行时重建描述。
|
||||
/// </param>
|
||||
/// <returns>当泛型定义和全部泛型实参都能表达时返回 <see langword="true" />。</returns>
|
||||
private static bool TryCreateConstructedGenericRuntimeTypeReference(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol genericNamedType,
|
||||
out RuntimeTypeReferenceSpec? runtimeTypeReference)
|
||||
{
|
||||
if (!TryCreateGenericTypeDefinitionReference(
|
||||
compilation,
|
||||
genericNamedType,
|
||||
out var genericTypeDefinitionReference))
|
||||
{
|
||||
runtimeTypeReference = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var genericTypeArguments =
|
||||
ImmutableArray.CreateBuilder<RuntimeTypeReferenceSpec>(genericNamedType.TypeArguments.Length);
|
||||
foreach (var typeArgument in genericNamedType.TypeArguments)
|
||||
{
|
||||
if (!TryCreateRuntimeTypeReference(compilation, typeArgument, out var genericTypeArgumentReference))
|
||||
{
|
||||
runtimeTypeReference = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
genericTypeArguments.Add(genericTypeArgumentReference!);
|
||||
}
|
||||
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromConstructedGeneric(
|
||||
genericTypeDefinitionReference!,
|
||||
genericTypeArguments.ToImmutable());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为无法直接书写的命名类型选择当前程序集反射查找或外部程序集反射查找表示。
|
||||
/// </summary>
|
||||
/// <param name="compilation">当前生成轮次的编译上下文。</param>
|
||||
/// <param name="namedType">需要在运行时解析的命名类型。</param>
|
||||
/// <param name="runtimeTypeReference">
|
||||
/// 当方法返回 <see langword="true" /> 时,包含适合写入生成注册器的命名类型运行时引用;
|
||||
/// 当返回 <see langword="false" /> 时,调用方应回退到更保守的注册路径。
|
||||
/// </param>
|
||||
/// <returns>当命名类型可安全编码为运行时引用时返回 <see langword="true" />。</returns>
|
||||
private static bool TryCreateNamedRuntimeTypeReference(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol namedType,
|
||||
out RuntimeTypeReferenceSpec? runtimeTypeReference)
|
||||
{
|
||||
return TryCreateReflectionLookupReference(
|
||||
compilation,
|
||||
namedType,
|
||||
GetReflectionTypeMetadataName(namedType),
|
||||
out runtimeTypeReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为已构造泛型类型解析其泛型定义的运行时引用描述。
|
||||
/// </summary>
|
||||
/// <param name="compilation">
|
||||
/// 当前编译上下文,用于判断泛型定义是否应以内联类型引用形式生成,或在运行时通过当前程序集反射解析。
|
||||
/// </param>
|
||||
/// <param name="genericNamedType">
|
||||
/// 已构造的泛型类型。该方法只处理其原始泛型定义,不负责递归解析类型实参。
|
||||
/// </param>
|
||||
/// <param name="genericTypeDefinitionReference">
|
||||
/// 当方法返回 <see langword="true" /> 时,包含泛型定义的直接引用或反射查找描述;
|
||||
/// 当方法返回 <see langword="false" /> 时为 <see langword="null" />,调用方应停止精确构造并回退到更保守的注册路径。
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// 当泛型定义能够以稳定方式编码到生成输出中时返回 <see langword="true" />;
|
||||
/// 若泛型定义既不能直接引用,也不属于当前程序集可供反射查找,则返回 <see langword="false" />。
|
||||
/// </returns>
|
||||
private static bool TryCreateGenericTypeDefinitionReference(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol genericNamedType,
|
||||
out RuntimeTypeReferenceSpec? genericTypeDefinitionReference)
|
||||
{
|
||||
var genericTypeDefinition = genericNamedType.OriginalDefinition;
|
||||
if (CanReferenceFromGeneratedRegistry(compilation, genericTypeDefinition))
|
||||
{
|
||||
genericTypeDefinitionReference = RuntimeTypeReferenceSpec.FromDirectReference(
|
||||
genericTypeDefinition
|
||||
.ConstructUnboundGenericType()
|
||||
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
|
||||
return true;
|
||||
}
|
||||
|
||||
return TryCreateReflectionLookupReference(
|
||||
compilation,
|
||||
genericTypeDefinition,
|
||||
GetReflectionTypeMetadataName(genericTypeDefinition),
|
||||
out genericTypeDefinitionReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为当前程序集或外部程序集中的命名类型构造统一的运行时反射查找描述。
|
||||
/// </summary>
|
||||
/// <param name="compilation">当前生成轮次的编译上下文。</param>
|
||||
/// <param name="namedType">需要在运行时解析的命名类型。</param>
|
||||
/// <param name="metadataName">写入生成代码的反射元数据名称。</param>
|
||||
/// <param name="runtimeTypeReference">成功时返回可直接写入注册器的运行时类型引用描述。</param>
|
||||
/// <returns>当命名类型具备可稳定编码的程序集归属信息时返回 <see langword="true" />。</returns>
|
||||
private static bool TryCreateReflectionLookupReference(
|
||||
Compilation compilation,
|
||||
INamedTypeSymbol namedType,
|
||||
string metadataName,
|
||||
out RuntimeTypeReferenceSpec? runtimeTypeReference)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(namedType.ContainingAssembly, compilation.Assembly))
|
||||
{
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromReflectionLookup(metadataName);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (namedType.ContainingAssembly is null)
|
||||
{
|
||||
runtimeTypeReference = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
runtimeTypeReference = RuntimeTypeReferenceSpec.FromExternalReflectionLookup(
|
||||
namedType.ContainingAssembly.Identity.ToString(),
|
||||
metadataName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CanReferenceFromGeneratedRegistry(Compilation compilation, ITypeSymbol type)
|
||||
{
|
||||
// Roslyn error symbols stringify to unresolved type names; emitting them via typeof(...) would turn
|
||||
// an existing user-code error into a second generator-produced compile error instead of falling back.
|
||||
if (type.TypeKind is TypeKind.Error or TypeKind.Dynamic)
|
||||
return false;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case IArrayTypeSymbol arrayType:
|
||||
return CanReferenceFromGeneratedRegistry(compilation, arrayType.ElementType);
|
||||
case INamedTypeSymbol namedType:
|
||||
if (!compilation.IsSymbolAccessibleWithin(namedType, compilation.Assembly, throughType: null))
|
||||
return false;
|
||||
|
||||
foreach (var typeArgument in namedType.TypeArguments)
|
||||
{
|
||||
if (!CanReferenceFromGeneratedRegistry(compilation, typeArgument))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
case IPointerTypeSymbol:
|
||||
case IFunctionPointerTypeSymbol:
|
||||
return false;
|
||||
case ITypeParameterSymbol:
|
||||
return false;
|
||||
default:
|
||||
// Remaining Roslyn type kinds that reach this branch have already been normalized by earlier guards
|
||||
// and can continue through the direct-reference path without emitting fallback reflection code.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsExternalAssemblyTypeLookup(RuntimeTypeReferenceSpec runtimeTypeReference)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName))
|
||||
return true;
|
||||
|
||||
if (runtimeTypeReference.ArrayElementTypeReference is not null &&
|
||||
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.ArrayElementTypeReference))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (runtimeTypeReference.PointerElementTypeReference is not null &&
|
||||
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.PointerElementTypeReference))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (runtimeTypeReference.GenericTypeDefinitionReference is not null &&
|
||||
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.GenericTypeDefinitionReference))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var genericTypeArgument in runtimeTypeReference.GenericTypeArguments)
|
||||
{
|
||||
if (ContainsExternalAssemblyTypeLookup(genericTypeArgument))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,846 @@
|
||||
namespace GFramework.Cqrs.SourceGenerators.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为当前编译程序集生成 CQRS 处理器注册器,以减少运行时的程序集反射扫描成本。
|
||||
/// </summary>
|
||||
public sealed partial class CqrsHandlerRegistryGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 生成程序集级 CQRS handler 注册器源码。
|
||||
/// </summary>
|
||||
/// <param name="generationEnvironment">
|
||||
/// 当前轮次的生成环境,用于决定 runtime 是否提供 <c>CqrsReflectionFallbackAttribute</c> 契约,以及是否需要在输出中发射对应的程序集级元数据。
|
||||
/// </param>
|
||||
/// <param name="registrations">
|
||||
/// 已整理并排序的 handler 注册描述。方法会据此生成 <c>CqrsHandlerRegistry.g.cs</c>,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。
|
||||
/// </param>
|
||||
/// <param name="fallbackHandlerTypeMetadataNames">
|
||||
/// 仍需依赖程序集级 reflection fallback 元数据恢复的 handler 元数据名称集合。
|
||||
/// 调用方必须先确保:若该集合非空,则 <paramref name="generationEnvironment" /> 已声明支持对应的 fallback attribute 契约;
|
||||
/// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。
|
||||
/// </param>
|
||||
/// <returns>完整的注册器源代码文本。</returns>
|
||||
/// <remarks>
|
||||
/// 当 <paramref name="fallbackHandlerTypeMetadataNames" /> 为空时,输出只包含程序集级 <c>CqrsHandlerRegistryAttribute</c> 和注册器实现。
|
||||
/// 当其非空且 runtime 合同可用时,输出还会附带程序集级 <c>CqrsReflectionFallbackAttribute</c>,让运行时补齐生成阶段无法精确表达的剩余 handler。
|
||||
/// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。
|
||||
/// </remarks>
|
||||
private static string GenerateSource(
|
||||
GenerationEnvironment generationEnvironment,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations,
|
||||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||||
{
|
||||
var sourceShape = CreateGeneratedRegistrySourceShape(registrations);
|
||||
var builder = new StringBuilder();
|
||||
AppendGeneratedSourcePreamble(builder, generationEnvironment, fallbackHandlerTypeMetadataNames);
|
||||
AppendGeneratedRegistryType(builder, registrations, sourceShape);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预先计算生成注册器需要的辅助分支,让主源码发射流程保持线性且避免重复扫描注册集合。
|
||||
/// </summary>
|
||||
/// <param name="registrations">已整理并排序的 handler 注册描述。</param>
|
||||
/// <returns>当前生成输出需要启用的结构分支。</returns>
|
||||
private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape(
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations)
|
||||
{
|
||||
var hasReflectedImplementationRegistrations = registrations.Any(static registration =>
|
||||
!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty);
|
||||
var hasPreciseReflectedRegistrations = registrations.Any(static registration =>
|
||||
!registration.PreciseReflectedRegistrations.IsDefaultOrEmpty);
|
||||
var hasReflectionTypeLookups = registrations.Any(static registration =>
|
||||
!string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName));
|
||||
var hasExternalAssemblyTypeLookups = registrations.Any(static registration =>
|
||||
registration.PreciseReflectedRegistrations.Any(static preciseRegistration =>
|
||||
preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup)));
|
||||
|
||||
return new GeneratedRegistrySourceShape(
|
||||
hasReflectedImplementationRegistrations,
|
||||
hasPreciseReflectedRegistrations,
|
||||
hasReflectionTypeLookups,
|
||||
hasExternalAssemblyTypeLookups);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射生成文件头、nullable 指令以及注册器所需的程序集级元数据特性。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="generationEnvironment">当前轮次的生成环境。</param>
|
||||
/// <param name="fallbackHandlerTypeMetadataNames">需要程序集级 reflection fallback 的 handler 元数据名称。</param>
|
||||
private static void AppendGeneratedSourcePreamble(
|
||||
StringBuilder builder,
|
||||
GenerationEnvironment generationEnvironment,
|
||||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||||
{
|
||||
builder.AppendLine("// <auto-generated />");
|
||||
builder.AppendLine("#nullable enable");
|
||||
builder.AppendLine();
|
||||
if (generationEnvironment.SupportsReflectionFallbackAttribute && fallbackHandlerTypeMetadataNames.Count > 0)
|
||||
{
|
||||
AppendReflectionFallbackAttribute(builder, fallbackHandlerTypeMetadataNames);
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
builder.Append("[assembly: global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::");
|
||||
builder.Append(GeneratedNamespace);
|
||||
builder.Append('.');
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.AppendLine("))]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射程序集级 reflection fallback 元数据特性,供运行时补齐生成阶段无法精确表达的 handler。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="fallbackHandlerTypeMetadataNames">需要写入特性的 handler 元数据名称。</param>
|
||||
private static void AppendReflectionFallbackAttribute(
|
||||
StringBuilder builder,
|
||||
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
|
||||
{
|
||||
builder.Append("[assembly: global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsReflectionFallbackAttribute(");
|
||||
for (var index = 0; index < fallbackHandlerTypeMetadataNames.Count; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
builder.Append(", ");
|
||||
|
||||
builder.Append('"');
|
||||
builder.Append(EscapeStringLiteral(fallbackHandlerTypeMetadataNames[index]));
|
||||
builder.Append('"');
|
||||
}
|
||||
|
||||
builder.AppendLine(")]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射生成注册器类型本体,包括 <c>Register</c> 方法和运行时反射辅助方法。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registrations">已排序的 handler 注册描述。</param>
|
||||
/// <param name="sourceShape">当前输出需要启用的结构分支。</param>
|
||||
private static void AppendGeneratedRegistryType(
|
||||
StringBuilder builder,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations,
|
||||
GeneratedRegistrySourceShape sourceShape)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append("namespace ");
|
||||
builder.Append(GeneratedNamespace);
|
||||
builder.AppendLine(";");
|
||||
builder.AppendLine();
|
||||
builder.Append("internal sealed class ");
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.Append(" : global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".ICqrsHandlerRegistry");
|
||||
builder.AppendLine("{");
|
||||
AppendRegisterMethod(builder, registrations, sourceShape);
|
||||
|
||||
if (sourceShape.HasExternalAssemblyTypeLookups)
|
||||
{
|
||||
builder.AppendLine();
|
||||
AppendReflectionHelpers(builder);
|
||||
}
|
||||
|
||||
builder.AppendLine("}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射注册器的 <c>Register</c> 方法,保持直接注册和反射注册之间的原始稳定排序。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registrations">已排序的 handler 注册描述。</param>
|
||||
/// <param name="sourceShape">当前输出需要启用的结构分支。</param>
|
||||
private static void AppendRegisterMethod(
|
||||
StringBuilder builder,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations,
|
||||
GeneratedRegistrySourceShape sourceShape)
|
||||
{
|
||||
builder.Append(
|
||||
" public void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::");
|
||||
builder.Append(LoggingNamespace);
|
||||
builder.AppendLine(".ILogger logger)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" if (services is null)");
|
||||
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(services));");
|
||||
builder.AppendLine(" if (logger is null)");
|
||||
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(logger));");
|
||||
if (sourceShape.RequiresRegistryAssemblyVariable)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append(" var registryAssembly = typeof(global::");
|
||||
builder.Append(GeneratedNamespace);
|
||||
builder.Append('.');
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.AppendLine(").Assembly;");
|
||||
}
|
||||
|
||||
if (registrations.Count > 0)
|
||||
builder.AppendLine();
|
||||
|
||||
for (var registrationIndex = 0; registrationIndex < registrations.Count; registrationIndex++)
|
||||
{
|
||||
var registration = registrations[registrationIndex];
|
||||
if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty ||
|
||||
!registration.PreciseReflectedRegistrations.IsDefaultOrEmpty)
|
||||
{
|
||||
AppendOrderedImplementationRegistrations(builder, registration, registrationIndex);
|
||||
}
|
||||
else if (!registration.DirectRegistrations.IsDefaultOrEmpty)
|
||||
{
|
||||
AppendDirectRegistrations(builder, registration);
|
||||
}
|
||||
}
|
||||
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void AppendDirectRegistrations(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration)
|
||||
{
|
||||
foreach (var directRegistration in registration.DirectRegistrations)
|
||||
{
|
||||
AppendServiceRegistration(
|
||||
builder,
|
||||
$"typeof({directRegistration.HandlerInterfaceDisplayName})",
|
||||
$"typeof({directRegistration.ImplementationTypeDisplayName})",
|
||||
" ");
|
||||
AppendRegistrationLog(
|
||||
builder,
|
||||
directRegistration.ImplementationLogName,
|
||||
directRegistration.HandlerInterfaceLogName,
|
||||
" ");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射 <c>AddTransient</c> 调用,调用方负责传入已经按当前分支解析好的 service 和 implementation 表达式。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="serviceTypeExpression">生成代码中的服务类型表达式。</param>
|
||||
/// <param name="implementationTypeExpression">生成代码中的实现类型表达式。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
private static void AppendServiceRegistration(
|
||||
StringBuilder builder,
|
||||
string serviceTypeExpression,
|
||||
string implementationTypeExpression,
|
||||
string indent)
|
||||
{
|
||||
builder.Append(indent);
|
||||
builder.AppendLine("global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(");
|
||||
builder.Append(indent);
|
||||
builder.AppendLine(" services,");
|
||||
builder.Append(indent);
|
||||
builder.Append(" ");
|
||||
builder.Append(serviceTypeExpression);
|
||||
builder.AppendLine(",");
|
||||
builder.Append(indent);
|
||||
builder.Append(" ");
|
||||
builder.Append(implementationTypeExpression);
|
||||
builder.AppendLine(");");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射与注册语句配套的调试日志,保持所有生成注册路径的日志文本完全一致。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="implementationLogName">实现类型日志名。</param>
|
||||
/// <param name="handlerInterfaceLogName">handler 接口日志名。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
private static void AppendRegistrationLog(
|
||||
StringBuilder builder,
|
||||
string implementationLogName,
|
||||
string handlerInterfaceLogName,
|
||||
string indent)
|
||||
{
|
||||
builder.Append(indent);
|
||||
builder.Append("logger.Debug(\"Registered CQRS handler ");
|
||||
builder.Append(EscapeStringLiteral(implementationLogName));
|
||||
builder.Append(" as ");
|
||||
builder.Append(EscapeStringLiteral(handlerInterfaceLogName));
|
||||
builder.AppendLine(".\");");
|
||||
}
|
||||
|
||||
private static void AppendOrderedImplementationRegistrations(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
int registrationIndex)
|
||||
{
|
||||
var orderedRegistrations = CreateOrderedRegistrations(registration);
|
||||
var implementationVariableName = $"implementationType{registrationIndex}";
|
||||
AppendImplementationTypeVariable(builder, registration, implementationVariableName);
|
||||
|
||||
builder.Append(" if (");
|
||||
builder.Append(implementationVariableName);
|
||||
builder.AppendLine(" is not null)");
|
||||
builder.AppendLine(" {");
|
||||
|
||||
foreach (var orderedRegistration in orderedRegistrations)
|
||||
{
|
||||
AppendOrderedRegistration(
|
||||
builder,
|
||||
registration,
|
||||
orderedRegistration,
|
||||
registrationIndex,
|
||||
implementationVariableName);
|
||||
}
|
||||
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合并直接注册、实现类型反射注册和精确反射注册,并按 handler 接口日志名排序以保持生成输出稳定。
|
||||
/// </summary>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <returns>带有来源类型和原始索引的有序注册列表。</returns>
|
||||
private static List<OrderedRegistrationSpec> CreateOrderedRegistrations(ImplementationRegistrationSpec registration)
|
||||
{
|
||||
var orderedRegistrations = new List<OrderedRegistrationSpec>(
|
||||
registration.DirectRegistrations.Length +
|
||||
registration.ReflectedImplementationRegistrations.Length +
|
||||
registration.PreciseReflectedRegistrations.Length);
|
||||
for (var directIndex = 0; directIndex < registration.DirectRegistrations.Length; directIndex++)
|
||||
{
|
||||
orderedRegistrations.Add(new OrderedRegistrationSpec(
|
||||
registration.DirectRegistrations[directIndex].HandlerInterfaceLogName,
|
||||
OrderedRegistrationKind.Direct,
|
||||
directIndex));
|
||||
}
|
||||
|
||||
for (var reflectedIndex = 0;
|
||||
reflectedIndex < registration.ReflectedImplementationRegistrations.Length;
|
||||
reflectedIndex++)
|
||||
{
|
||||
orderedRegistrations.Add(new OrderedRegistrationSpec(
|
||||
registration.ReflectedImplementationRegistrations[reflectedIndex].HandlerInterfaceLogName,
|
||||
OrderedRegistrationKind.ReflectedImplementation,
|
||||
reflectedIndex));
|
||||
}
|
||||
|
||||
for (var preciseIndex = 0;
|
||||
preciseIndex < registration.PreciseReflectedRegistrations.Length;
|
||||
preciseIndex++)
|
||||
{
|
||||
orderedRegistrations.Add(new OrderedRegistrationSpec(
|
||||
registration.PreciseReflectedRegistrations[preciseIndex].HandlerInterfaceLogName,
|
||||
OrderedRegistrationKind.PreciseReflected,
|
||||
preciseIndex));
|
||||
}
|
||||
|
||||
orderedRegistrations.Sort(static (left, right) =>
|
||||
StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName));
|
||||
return orderedRegistrations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射实现类型变量。公开类型直接使用 <c>typeof</c>,不可直接引用的实现类型则从当前程序集反射解析。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
|
||||
private static void AppendImplementationTypeVariable(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
string implementationVariableName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName))
|
||||
{
|
||||
builder.Append(" var ");
|
||||
builder.Append(implementationVariableName);
|
||||
builder.Append(" = typeof(");
|
||||
builder.Append(registration.ImplementationTypeDisplayName);
|
||||
builder.AppendLine(");");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(" var ");
|
||||
builder.Append(implementationVariableName);
|
||||
builder.Append(" = registryAssembly.GetType(\"");
|
||||
builder.Append(EscapeStringLiteral(registration.ReflectionTypeMetadataName!));
|
||||
builder.AppendLine("\", throwOnError: false, ignoreCase: false);");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据注册来源发射单条有序注册,确保混合直接和反射路径时仍按 handler 接口名稳定输出。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <param name="orderedRegistration">带来源类型和原始索引的排序项。</param>
|
||||
/// <param name="registrationIndex">实现类型在整体注册列表中的索引,用于生成稳定变量名。</param>
|
||||
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
|
||||
private static void AppendOrderedRegistration(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
OrderedRegistrationSpec orderedRegistration,
|
||||
int registrationIndex,
|
||||
string implementationVariableName)
|
||||
{
|
||||
switch (orderedRegistration.Kind)
|
||||
{
|
||||
case OrderedRegistrationKind.Direct:
|
||||
AppendOrderedDirectRegistration(
|
||||
builder,
|
||||
registration,
|
||||
registration.DirectRegistrations[orderedRegistration.Index],
|
||||
implementationVariableName);
|
||||
break;
|
||||
case OrderedRegistrationKind.ReflectedImplementation:
|
||||
AppendOrderedReflectedImplementationRegistration(
|
||||
builder,
|
||||
registration,
|
||||
registration.ReflectedImplementationRegistrations[orderedRegistration.Index],
|
||||
implementationVariableName);
|
||||
break;
|
||||
case OrderedRegistrationKind.PreciseReflected:
|
||||
AppendOrderedPreciseReflectedRegistration(
|
||||
builder,
|
||||
registration,
|
||||
registration.PreciseReflectedRegistrations[orderedRegistration.Index],
|
||||
registrationIndex,
|
||||
orderedRegistration.Index,
|
||||
implementationVariableName);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported ordered CQRS registration kind {orderedRegistration.Kind}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射实现类型已通过变量解析、handler 接口可直接引用的直接注册语句。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <param name="directRegistration">当前直接注册项。</param>
|
||||
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
|
||||
private static void AppendOrderedDirectRegistration(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
HandlerRegistrationSpec directRegistration,
|
||||
string implementationVariableName)
|
||||
{
|
||||
AppendServiceRegistration(
|
||||
builder,
|
||||
$"typeof({directRegistration.HandlerInterfaceDisplayName})",
|
||||
implementationVariableName,
|
||||
" ");
|
||||
AppendRegistrationLog(
|
||||
builder,
|
||||
registration.ImplementationLogName,
|
||||
directRegistration.HandlerInterfaceLogName,
|
||||
" ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射实现类型需要反射解析、handler 接口可直接引用的注册语句。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <param name="reflectedRegistration">当前实现类型反射注册项。</param>
|
||||
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
|
||||
private static void AppendOrderedReflectedImplementationRegistration(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
ReflectedImplementationRegistrationSpec reflectedRegistration,
|
||||
string implementationVariableName)
|
||||
{
|
||||
AppendServiceRegistration(
|
||||
builder,
|
||||
$"typeof({reflectedRegistration.HandlerInterfaceDisplayName})",
|
||||
implementationVariableName,
|
||||
" ");
|
||||
AppendRegistrationLog(
|
||||
builder,
|
||||
registration.ImplementationLogName,
|
||||
reflectedRegistration.HandlerInterfaceLogName,
|
||||
" ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射 handler 接口需要运行时精确构造的注册语句。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registration">单个实现类型聚合后的注册描述。</param>
|
||||
/// <param name="preciseRegistration">当前精确反射注册项。</param>
|
||||
/// <param name="registrationIndex">实现类型在整体注册列表中的索引。</param>
|
||||
/// <param name="orderedRegistrationIndex">当前注册项在原始精确反射注册集合中的索引。</param>
|
||||
/// <param name="implementationVariableName">生成代码中的实现类型变量名。</param>
|
||||
private static void AppendOrderedPreciseReflectedRegistration(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration,
|
||||
PreciseReflectedRegistrationSpec preciseRegistration,
|
||||
int registrationIndex,
|
||||
int orderedRegistrationIndex,
|
||||
string implementationVariableName)
|
||||
{
|
||||
var registrationVariablePrefix = $"serviceType{registrationIndex}_{orderedRegistrationIndex}";
|
||||
AppendPreciseReflectedTypeResolution(
|
||||
builder,
|
||||
preciseRegistration.ServiceTypeArguments,
|
||||
registrationVariablePrefix,
|
||||
implementationVariableName,
|
||||
preciseRegistration.OpenHandlerTypeDisplayName,
|
||||
registration.ImplementationLogName,
|
||||
preciseRegistration.HandlerInterfaceLogName,
|
||||
3);
|
||||
}
|
||||
|
||||
private static void AppendPreciseReflectedTypeResolution(
|
||||
StringBuilder builder,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> serviceTypeArguments,
|
||||
string registrationVariablePrefix,
|
||||
string implementationVariableName,
|
||||
string openHandlerTypeDisplayName,
|
||||
string implementationLogName,
|
||||
string handlerInterfaceLogName,
|
||||
int indentLevel)
|
||||
{
|
||||
var indent = new string(' ', indentLevel * 4);
|
||||
var reflectedArgumentNames = new List<string>();
|
||||
var resolvedArgumentNames = AppendServiceTypeArgumentResolutions(
|
||||
builder,
|
||||
serviceTypeArguments,
|
||||
registrationVariablePrefix,
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
if (reflectedArgumentNames.Count > 0)
|
||||
indent = AppendReflectedArgumentGuardStart(builder, reflectedArgumentNames, indent);
|
||||
|
||||
AppendClosedGenericServiceTypeCreation(
|
||||
builder,
|
||||
registrationVariablePrefix,
|
||||
openHandlerTypeDisplayName,
|
||||
resolvedArgumentNames,
|
||||
indent);
|
||||
AppendServiceRegistration(builder, registrationVariablePrefix, implementationVariableName, indent);
|
||||
AppendRegistrationLog(builder, implementationLogName, handlerInterfaceLogName, indent);
|
||||
|
||||
if (reflectedArgumentNames.Count > 0)
|
||||
{
|
||||
builder.Append(new string(' ', indentLevel * 4));
|
||||
builder.AppendLine("}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归发射每个 handler 泛型实参的运行时类型解析表达式。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="serviceTypeArguments">handler 服务类型的运行时泛型实参描述。</param>
|
||||
/// <param name="registrationVariablePrefix">当前注册项的稳定变量名前缀。</param>
|
||||
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
/// <returns>可传给 <c>MakeGenericType</c> 的实参表达式。</returns>
|
||||
private static string[] AppendServiceTypeArgumentResolutions(
|
||||
StringBuilder builder,
|
||||
ImmutableArray<RuntimeTypeReferenceSpec> serviceTypeArguments,
|
||||
string registrationVariablePrefix,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
var resolvedArgumentNames = new string[serviceTypeArguments.Length];
|
||||
for (var argumentIndex = 0; argumentIndex < serviceTypeArguments.Length; argumentIndex++)
|
||||
{
|
||||
resolvedArgumentNames[argumentIndex] = AppendRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
serviceTypeArguments[argumentIndex],
|
||||
$"{registrationVariablePrefix}Argument{argumentIndex}",
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
}
|
||||
|
||||
return resolvedArgumentNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为运行时反射解析出的泛型实参发射空值保护块,避免生成注册器注册无法完整构造的服务类型。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="reflectedArgumentNames">需要参与空值检查的变量名。</param>
|
||||
/// <param name="indent">保护块开始前的缩进。</param>
|
||||
/// <returns>保护块内部应使用的下一层缩进。</returns>
|
||||
private static string AppendReflectedArgumentGuardStart(
|
||||
StringBuilder builder,
|
||||
IReadOnlyList<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
builder.Append(indent);
|
||||
builder.Append("if (");
|
||||
for (var index = 0; index < reflectedArgumentNames.Count; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
builder.Append(" && ");
|
||||
|
||||
builder.Append(reflectedArgumentNames[index]);
|
||||
builder.Append(" is not null");
|
||||
}
|
||||
|
||||
builder.AppendLine(")");
|
||||
builder.Append(indent);
|
||||
builder.AppendLine("{");
|
||||
return $"{indent} ";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射关闭 handler 服务类型的 <c>MakeGenericType</c> 构造语句。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="registrationVariablePrefix">生成代码中的服务类型变量名。</param>
|
||||
/// <param name="openHandlerTypeDisplayName">开放 handler 接口类型显示名。</param>
|
||||
/// <param name="resolvedArgumentNames">已解析的泛型实参表达式。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
private static void AppendClosedGenericServiceTypeCreation(
|
||||
StringBuilder builder,
|
||||
string registrationVariablePrefix,
|
||||
string openHandlerTypeDisplayName,
|
||||
IReadOnlyList<string> resolvedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
builder.Append(indent);
|
||||
builder.Append("var ");
|
||||
builder.Append(registrationVariablePrefix);
|
||||
builder.Append(" = typeof(");
|
||||
builder.Append(openHandlerTypeDisplayName);
|
||||
builder.Append(").MakeGenericType(");
|
||||
for (var index = 0; index < resolvedArgumentNames.Count; index++)
|
||||
{
|
||||
if (index > 0)
|
||||
builder.Append(", ");
|
||||
|
||||
builder.Append(resolvedArgumentNames[index]);
|
||||
}
|
||||
|
||||
builder.AppendLine(");");
|
||||
}
|
||||
|
||||
private static string AppendRuntimeTypeReferenceResolution(
|
||||
StringBuilder builder,
|
||||
RuntimeTypeReferenceSpec runtimeTypeReference,
|
||||
string variableBaseName,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(runtimeTypeReference.TypeDisplayName))
|
||||
return $"typeof({runtimeTypeReference.TypeDisplayName})";
|
||||
|
||||
if (runtimeTypeReference.ArrayElementTypeReference is not null)
|
||||
return AppendArrayRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference,
|
||||
variableBaseName,
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
if (runtimeTypeReference.PointerElementTypeReference is not null)
|
||||
return AppendPointerRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference,
|
||||
variableBaseName,
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
if (runtimeTypeReference.GenericTypeDefinitionReference is not null)
|
||||
return AppendConstructedGenericRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference,
|
||||
variableBaseName,
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
return AppendReflectionRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference,
|
||||
variableBaseName,
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射数组类型引用的运行时重建表达式。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="runtimeTypeReference">数组类型引用描述。</param>
|
||||
/// <param name="variableBaseName">用于递归生成变量名的稳定前缀。</param>
|
||||
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
/// <returns>数组类型表达式。</returns>
|
||||
private static string AppendArrayRuntimeTypeReferenceResolution(
|
||||
StringBuilder builder,
|
||||
RuntimeTypeReferenceSpec runtimeTypeReference,
|
||||
string variableBaseName,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
var elementExpression = AppendRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference.ArrayElementTypeReference!,
|
||||
$"{variableBaseName}Element",
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
return runtimeTypeReference.ArrayRank == 1
|
||||
? $"{elementExpression}.MakeArrayType()"
|
||||
: $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射指针类型引用的运行时重建表达式。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="runtimeTypeReference">指针类型引用描述。</param>
|
||||
/// <param name="variableBaseName">用于递归生成变量名的稳定前缀。</param>
|
||||
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
/// <returns>指针类型表达式。</returns>
|
||||
private static string AppendPointerRuntimeTypeReferenceResolution(
|
||||
StringBuilder builder,
|
||||
RuntimeTypeReferenceSpec runtimeTypeReference,
|
||||
string variableBaseName,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
var pointedAtExpression = AppendRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference.PointerElementTypeReference!,
|
||||
$"{variableBaseName}PointedAt",
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
|
||||
return $"{pointedAtExpression}.MakePointerType()";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射已构造泛型类型引用的运行时重建表达式。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="runtimeTypeReference">已构造泛型类型引用描述。</param>
|
||||
/// <param name="variableBaseName">用于递归生成变量名的稳定前缀。</param>
|
||||
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
/// <returns>已构造泛型类型表达式。</returns>
|
||||
private static string AppendConstructedGenericRuntimeTypeReferenceResolution(
|
||||
StringBuilder builder,
|
||||
RuntimeTypeReferenceSpec runtimeTypeReference,
|
||||
string variableBaseName,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
var genericTypeDefinitionExpression = AppendRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference.GenericTypeDefinitionReference!,
|
||||
$"{variableBaseName}GenericDefinition",
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
var genericArgumentExpressions = new string[runtimeTypeReference.GenericTypeArguments.Length];
|
||||
for (var argumentIndex = 0;
|
||||
argumentIndex < runtimeTypeReference.GenericTypeArguments.Length;
|
||||
argumentIndex++)
|
||||
{
|
||||
genericArgumentExpressions[argumentIndex] = AppendRuntimeTypeReferenceResolution(
|
||||
builder,
|
||||
runtimeTypeReference.GenericTypeArguments[argumentIndex],
|
||||
$"{variableBaseName}GenericArgument{argumentIndex}",
|
||||
reflectedArgumentNames,
|
||||
indent);
|
||||
}
|
||||
|
||||
return $"{genericTypeDefinitionExpression}.MakeGenericType({string.Join(", ", genericArgumentExpressions)})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发射命名类型的运行时反射查找语句,并返回后续服务类型构造应引用的变量名。
|
||||
/// </summary>
|
||||
/// <param name="builder">生成源码构造器。</param>
|
||||
/// <param name="runtimeTypeReference">反射查找类型引用描述。</param>
|
||||
/// <param name="variableBaseName">生成代码中的反射变量名。</param>
|
||||
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
|
||||
/// <param name="indent">当前生成语句的缩进。</param>
|
||||
/// <returns>生成代码中的反射变量名。</returns>
|
||||
private static string AppendReflectionRuntimeTypeReferenceResolution(
|
||||
StringBuilder builder,
|
||||
RuntimeTypeReferenceSpec runtimeTypeReference,
|
||||
string variableBaseName,
|
||||
ICollection<string> reflectedArgumentNames,
|
||||
string indent)
|
||||
{
|
||||
reflectedArgumentNames.Add(variableBaseName);
|
||||
builder.Append(indent);
|
||||
builder.Append("var ");
|
||||
builder.Append(variableBaseName);
|
||||
if (string.IsNullOrWhiteSpace(runtimeTypeReference.ReflectionAssemblyName))
|
||||
{
|
||||
builder.Append(" = registryAssembly.GetType(\"");
|
||||
builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!));
|
||||
builder.AppendLine("\", throwOnError: false, ignoreCase: false);");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(" = ResolveReferencedAssemblyType(\"");
|
||||
builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionAssemblyName!));
|
||||
builder.Append("\", \"");
|
||||
builder.Append(EscapeStringLiteral(runtimeTypeReference.ReflectionTypeMetadataName!));
|
||||
builder.AppendLine("\");");
|
||||
}
|
||||
|
||||
return variableBaseName;
|
||||
}
|
||||
|
||||
private static void AppendReflectionHelpers(StringBuilder builder)
|
||||
{
|
||||
builder.AppendLine(
|
||||
" private static global::System.Type? ResolveReferencedAssemblyType(string assemblyIdentity, string typeMetadataName)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" var assembly = ResolveReferencedAssembly(assemblyIdentity);");
|
||||
builder.AppendLine(
|
||||
" return assembly?.GetType(typeMetadataName, throwOnError: false, ignoreCase: false);");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(
|
||||
" private static global::System.Reflection.Assembly? ResolveReferencedAssembly(string assemblyIdentity)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" global::System.Reflection.AssemblyName targetAssemblyName;");
|
||||
builder.AppendLine(" try");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(
|
||||
" targetAssemblyName = new global::System.Reflection.AssemblyName(assemblyIdentity);");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine(" catch");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" return null;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(
|
||||
" foreach (var assembly in global::System.AppDomain.CurrentDomain.GetAssemblies())");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(
|
||||
" if (global::System.Reflection.AssemblyName.ReferenceMatchesDefinition(targetAssemblyName, assembly.GetName()))");
|
||||
builder.AppendLine(" return assembly;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" try");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(
|
||||
" return global::System.Reflection.Assembly.Load(targetAssemblyName);");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine(" catch");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" return null;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static string EscapeStringLiteral(string value)
|
||||
{
|
||||
return value.Replace("\\", "\\\\")
|
||||
.Replace("\"", "\\\"")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace("\r", "\\r");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -131,6 +131,20 @@ Scene 与 UI 路由共享这套基础约定。
|
||||
- `Enums/`
|
||||
- UI/Scene 转场、UI 层级、输入动作、存储类型等公共枚举
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-23` 对 `GFramework.Game.Abstractions` 做的一轮轻量 XML 盘点结果:只统计公开 /
|
||||
内部类型声明是否带 XML 注释,用来建立契约层阅读入口;成员级参数、返回值、异常和生命周期说明仍需要在后续 API 波次继续细化。
|
||||
|
||||
| 契约族 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Config/` | `7/7` 个类型声明已带 XML 注释 | `IConfigLoader`、`IConfigRegistry`、`IConfigTable<TKey, TValue>`、`ConfigLoadException` | 看配置表注册、读取约定和失败诊断模型 |
|
||||
| `Data/` | `14/14` 个类型声明已带 XML 注释 | `IDataRepository`、`ISettingsDataRepository`、`ISaveRepository<TSaveData>`、`DataRepositoryOptions` | 看业务数据、设置持久化、槽位存档和版本迁移契约 |
|
||||
| `Setting/` | `12/12` 个类型声明已带 XML 注释 | `ISettingsData`、`ISettingsModel`、`ISettingsSystem`、`LocalizationSettings` | 看设置数据、应用语义、迁移接口和内置设置对象 |
|
||||
| `Scene/` | `14/14` 个类型声明已带 XML 注释 | `IScene`、`ISceneRouter`、`ISceneFactory`、`SceneTransitionEvent` | 看场景行为、路由、工厂 / root 边界与转场事件模型 |
|
||||
| `UI/` | `19/19` 个类型声明已带 XML 注释 | `IUiPage`、`IUiRouter`、`IUiFactory`、`UiInteractionProfile`、`UiTransitionHandlerOptions` | 看页面栈、层级 UI、输入动作与 UI 转场契约 |
|
||||
| `Routing/` `Storage/` `Asset/` `Enums/` | `13/13` 个类型声明已带 XML 注释 | `IRoute`、`IRouteContext`、`IFileStorage`、`IAssetRegistry<T>`、`UiLayer`、`SceneTransitionType` | 看公共路由上下文、存储角色、资源注册表与跨层共享枚举 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 只想在公共业务层声明游戏对象
|
||||
|
||||
@ -18,3 +18,4 @@
|
||||
GF_ConfigSchema_011 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_012 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_013 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
GF_ConfigSchema_014 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -151,4 +151,15 @@ public static class ConfigSchemaDiagnostics
|
||||
SourceGeneratorsConfigCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// schema 字段名在标识符归一化后发生冲突。
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor DuplicateGeneratedIdentifier = new(
|
||||
"GF_ConfigSchema_014",
|
||||
"Config schema property names collide after C# identifier normalization",
|
||||
"Property '{1}' in schema file '{0}' uses schema key '{2}', which generates duplicate C# identifier '{3}' already produced by schema key '{4}'",
|
||||
SourceGeneratorsConfigCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
}
|
||||
|
||||
@ -44,6 +44,15 @@ GameProject/
|
||||
|
||||
默认情况下,打包产物会通过 `targets` 把 `schemas/**/*.schema.json` 纳入 `AdditionalFiles`。
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-23` 对 `GFramework.Game.SourceGenerators` 做的一轮轻量 XML 盘点结果:只统计公开类型声明是否带 XML 注释,用来建立生成器入口;具体诊断消息、生成输出和兼容性语义仍需要回到源码与测试继续核对。
|
||||
|
||||
| 类型族 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Config/` | `1/1` 个类型声明已带 XML 注释 | `SchemaConfigGenerator` | 看 schema 到配置类型 / 表包装 / 注册辅助代码的生成入口 |
|
||||
| `Diagnostics/` | `1/1` 个类型声明已带 XML 注释 | `ConfigSchemaDiagnostics` | 看生成器会抛出的诊断类别与失败边界 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
```xml
|
||||
|
||||
@ -169,6 +169,19 @@
|
||||
|
||||
这两部分一般被上层子系统消费,不是多数项目的第一接入点。
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-23` 对 `GFramework.Game` 做的一轮轻量 XML 盘点结果:只统计公开 /
|
||||
内部类型声明是否带 XML 注释,用来建立运行时阅读入口;成员级参数、返回值、异常和生命周期说明仍需要在后续 API 波次继续细化。
|
||||
|
||||
| 子系统 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Config/` | `26/26` 个类型声明已带 XML 注释 | `YamlConfigLoader`、`ConfigRegistry`、`GameConfigBootstrap`、`YamlConfigSchemaValidator` | 看 YAML 加载、schema 校验、模块接入与热重载边界 |
|
||||
| `Data/` `Storage/` `Serializer/` | `8/8` 个类型声明已带 XML 注释 | `DataRepository`、`SaveRepository<TSaveData>`、`UnifiedSettingsDataRepository`、`FileStorage`、`JsonSerializer` | 看持久化布局、槽位存档、统一设置文件和底层序列化 / 存储实现 |
|
||||
| `Setting/` | `9/9` 个类型声明已带 XML 注释 | `SettingsModel<TRepository>`、`SettingsSystem`、`SettingsAppliedEvent<T>` | 看初始化、应用、保存、重置等设置生命周期编排 |
|
||||
| `Scene/` `UI/` `Routing/` | `10/10` 个类型声明已带 XML 注释 | `SceneRouterBase`、`UiRouterBase`、`SceneTransitionPipeline`、`UiTransitionPipeline`、`RouterBase<TRoute, TContext>` | 看路由基类、转换处理器和项目层需要自己提供的 factory / root 边界 |
|
||||
| `Extensions/` `Internal/` `State/` | `3/3` 个类型声明已带 XML 注释 | `DataLocationExtensions`、`VersionedMigrationRunner`、`GameStateMachineSystem` | 看辅助扩展、内部迁移执行逻辑和游戏态状态机封装 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
下面按最常见的四种接入目标给出最短路径。
|
||||
|
||||
@ -1,181 +1,176 @@
|
||||
# GFramework.Godot.SourceGenerators
|
||||
|
||||
面向 Godot 场景的源码生成扩展模块,减少模板代码。
|
||||
`GFramework.Godot.SourceGenerators` 负责把 Godot 项目里的重复样板迁移到编译期。
|
||||
|
||||
## 主要功能
|
||||
当前包覆盖三类核心场景:
|
||||
|
||||
- 与 Godot 场景相关的编译期生成能力
|
||||
- 基于 Roslyn 的增量生成器实现
|
||||
- `project.godot` 项目元数据生成,产出 AutoLoad 与 Input Action 的强类型访问入口
|
||||
- `[GetNode]` 字段注入,减少 `_Ready()` 里的 `GetNode<T>()` 样板代码
|
||||
- `[BindNodeSignal]` 方法绑定,减少 `_Ready()` / `_ExitTree()` 中重复的事件订阅样板代码
|
||||
- `project.godot` 元数据入口:生成 `AutoLoads` 与 `InputActions`
|
||||
- 节点字段与信号接线:`[GetNode]`、`[BindNodeSignal]`
|
||||
- Scene / UI 与启动注册样板:`[AutoScene]`、`[AutoUiPage]`、`[AutoRegisterExportedCollections]`
|
||||
|
||||
## 使用建议
|
||||
它是 Analyzer 包,不是运行时库。
|
||||
|
||||
- 仅在 Godot + C# 项目中启用
|
||||
- 非 Godot 项目可只使用 GFramework.SourceGenerators
|
||||
- 当项目通过 NuGet 包引用本模块时,根目录下的 `project.godot` 会被自动加入 `AdditionalFiles`
|
||||
- 当项目通过 `ProjectReference(OutputItemType=Analyzer)` 直接引用生成器时,需要手动把 `project.godot` 加入
|
||||
`AdditionalFiles`
|
||||
## 包定位
|
||||
|
||||
## project.godot 集成
|
||||
当前生成器主要减少这些重复代码:
|
||||
|
||||
默认情况下,生成器会读取 Godot 项目根目录下的 `project.godot`,并生成:
|
||||
- 从 `project.godot` 手写 AutoLoad / Input Action 字符串
|
||||
- 在 `_Ready()` 里重复写 `GetNode<T>()`
|
||||
- 在 `_Ready()` / `_ExitTree()` 里重复写 CLR event 订阅与解绑
|
||||
- 为 Godot 场景根节点和页面根节点重复声明 `GetScene()` / `GetPage()` 样板
|
||||
- 在启动入口里重复遍历导出集合并逐项注册到 registry
|
||||
|
||||
它不负责:
|
||||
|
||||
- 提供运行时 Scene / UI / 配置实现
|
||||
- 自动接管完整生命周期方法
|
||||
- 代替 `GFramework.Godot` 的宿主适配逻辑
|
||||
|
||||
## 与相邻包的关系
|
||||
|
||||
- `GFramework.Godot`
|
||||
- 负责 Godot 运行时适配。
|
||||
- 本包只负责编译期入口和样板生成。
|
||||
- `GFramework.Godot.SourceGenerators.Abstractions`
|
||||
- 特性定义所在位置。
|
||||
- 当前 `IsPackable=false`,按内部支撑模块处理,不作为独立消费包推广。
|
||||
- `GFramework.SourceGenerators.Common`
|
||||
- 提供公共生成器基础设施与部分类级诊断支持。
|
||||
- 同样按内部支撑模块处理。
|
||||
|
||||
## 子系统地图
|
||||
|
||||
### `GodotProjectMetadataGenerator`
|
||||
|
||||
读取 `project.godot`,生成:
|
||||
|
||||
- `GFramework.Godot.Generated.AutoLoads`
|
||||
- `GFramework.Godot.Generated.InputActions`
|
||||
|
||||
如果你需要覆盖默认项目文件路径,可以在 MSBuild 中设置:
|
||||
这是项目级元数据入口,不处理节点字段注入或信号绑定。
|
||||
|
||||
- 路径可以调整到项目根目录下的其他位置
|
||||
- 文件名必须仍然是 `project.godot`,否则生成器会发出警告并忽略该文件
|
||||
### `GetNodeGenerator` 与 `BindNodeSignalGenerator`
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<GFrameworkGodotProjectFile>Config/project.godot</GFrameworkGodotProjectFile>
|
||||
</PropertyGroup>
|
||||
```
|
||||
- `[GetNode]` 负责生成节点字段注入代码
|
||||
- `[BindNodeSignal]` 负责生成 CLR event 绑定 / 解绑辅助方法
|
||||
|
||||
如果你在仓库内通过 analyzer 形式直接引用本项目,则需要显式配置:
|
||||
这两项能力通常一起使用,但职责不同:
|
||||
|
||||
- `[GetNode]` 解决“怎么拿到字段实例”
|
||||
- `[BindNodeSignal]` 解决“字段可用后怎么订阅 / 解绑事件”
|
||||
|
||||
### `Behavior/`
|
||||
|
||||
- `AutoSceneGenerator`
|
||||
- `AutoUiPageGenerator`
|
||||
|
||||
用于给场景根节点和 UI 页面根节点生成稳定的 `GetScene()` / `GetPage()` 包装入口。
|
||||
|
||||
### `Registration/`
|
||||
|
||||
- `AutoRegisterExportedCollectionsGenerator`
|
||||
|
||||
用于把“遍历导出集合并逐项调用 registry 方法”的启动样板收敛成生成方法。
|
||||
|
||||
### `Diagnostics/`
|
||||
|
||||
当前诊断围绕这些方向组织:
|
||||
|
||||
- `project.godot` 文件与元数据约束
|
||||
- `GetNode` / `BindNodeSignal` 的目标成员合法性
|
||||
- `AutoScene` / `AutoUiPage` 的宿主类型与参数合法性
|
||||
- 导出集合注册的成员形状与方法匹配约束
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 安装生成器包
|
||||
|
||||
常规 NuGet 引用方式:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GeWuYou.GFramework.Godot.SourceGenerators"
|
||||
Version="x.y.z"
|
||||
PrivateAssets="all"
|
||||
ExcludeAssets="runtime" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
通常还会同时引用:
|
||||
|
||||
```xml
|
||||
<PackageReference Include="GeWuYou.GFramework.Godot" Version="x.y.z" />
|
||||
```
|
||||
|
||||
### 2. 让 `project.godot` 进入 `AdditionalFiles`
|
||||
|
||||
通过 NuGet 包使用时,`GeWuYou.GFramework.Godot.SourceGenerators.targets` 会自动尝试把项目根目录下的 `project.godot`
|
||||
加入 `AdditionalFiles`。
|
||||
|
||||
如果你是仓库内直接通过 `ProjectReference(OutputItemType=Analyzer)` 引用生成器项目,需要手动加入:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GFramework.Godot.SourceGenerators\GFramework.Godot.SourceGenerators.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
<AdditionalFiles Include="project.godot" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
## AutoLoad 强类型访问
|
||||
### 3. 在节点脚本里显式接生成方法
|
||||
|
||||
当某个 AutoLoad 无法仅靠类型名唯一推断到 C# 节点类型时,可以使用 `[AutoLoad]` 显式声明映射:
|
||||
当前最重要的生命周期约束是:
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
- `[GetNode]` 在类型手写 `_Ready()` 时,需要显式调用 `__InjectGetNodes_Generated()`
|
||||
- `[BindNodeSignal]` 在手写 `_Ready()` / `_ExitTree()` 时,需要显式调用
|
||||
`__BindNodeSignals_Generated()` 与 `__UnbindNodeSignals_Generated()`
|
||||
- `[AutoScene]`、`[AutoUiPage]`、`[AutoRegisterExportedCollections]` 都只生成辅助入口,不会替你织入生命周期
|
||||
|
||||
[AutoLoad("GameServices")]
|
||||
public partial class GameServices : Node
|
||||
{
|
||||
}
|
||||
```
|
||||
也就是说,本包负责生成辅助方法,但调用时机仍由项目侧决定。
|
||||
|
||||
对应 `project.godot`:
|
||||
### 4. 按场景选特性
|
||||
|
||||
```ini
|
||||
[autoload]
|
||||
GameServices="*res://autoload/game_services.tscn"
|
||||
AudioBus="*res://autoload/audio_bus.gd"
|
||||
```
|
||||
- 项目级元数据:
|
||||
- `project.godot` -> `AutoLoads`、`InputActions`
|
||||
- 固定节点字段:
|
||||
- `[GetNode]`
|
||||
- 固定 CLR event 订阅:
|
||||
- `[BindNodeSignal]`
|
||||
- Godot 场景根节点:
|
||||
- `[AutoScene]`
|
||||
- Godot UI 页面根节点:
|
||||
- `[AutoUiPage]`
|
||||
- 启动入口中的集合批量注册:
|
||||
- `[AutoRegisterExportedCollections]`
|
||||
|
||||
生成器会产出统一入口:
|
||||
## 当前约束
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.Generated;
|
||||
- `GFrameworkGodotProjectFile` 可以改相对路径,但文件名必须仍然是 `project.godot`
|
||||
- `[GetNode]` 与 `[BindNodeSignal]` 都要求宿主类型是顶层 `partial class`
|
||||
- `[BindNodeSignal]` 面向 CLR event,不会自动调用 `Connect()` / `Disconnect()`
|
||||
- `[AutoScene]` 与 `[AutoUiPage]` 只生成行为包装入口,不会替代 `SceneRouterBase` 或 `UiRouterBase`
|
||||
- `[AutoRegisterExportedCollections]` 只适合“集合 -> registry -> 单参数注册方法”这类稳定形状
|
||||
|
||||
var gameServices = AutoLoads.GameServices;
|
||||
## 文档入口
|
||||
|
||||
if (AutoLoads.TryGetAudioBus(out var audioBus))
|
||||
{
|
||||
}
|
||||
```
|
||||
- 生成器总览:[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)
|
||||
- 集成教程:[docs/zh-CN/tutorials/godot-integration.md](../docs/zh-CN/tutorials/godot-integration.md)
|
||||
|
||||
- 显式 `[AutoLoad]` 映射优先于隐式类型名推断
|
||||
- 若同名映射冲突,生成器会给出诊断并退化为 `Godot.Node` 访问
|
||||
- 若无法映射到 C# 节点类型,仍会生成可用的 `Godot.Node` 访问器
|
||||
## 什么时候不该先看这个包
|
||||
|
||||
## Input Action 常量生成
|
||||
以下场景更适合先回到其他入口:
|
||||
|
||||
`project.godot` 的 `[input]` 段会自动生成稳定常量,避免手写字符串:
|
||||
|
||||
```ini
|
||||
[input]
|
||||
move_up={
|
||||
}
|
||||
ui_cancel={
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.Generated;
|
||||
|
||||
if (Input.IsActionJustPressed(InputActions.MoveUp))
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
- 动作名会转换为可补全的 C# 标识符,例如 `move_up -> MoveUp`
|
||||
- 当多个动作名映射到同一标识符时,会追加稳定后缀并给出警告
|
||||
|
||||
## GetNode 用法
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
|
||||
public partial class TopBar : HBoxContainer
|
||||
{
|
||||
[GetNode]
|
||||
private HBoxContainer _leftContainer = null!;
|
||||
|
||||
[GetNode]
|
||||
private HBoxContainer _rightContainer = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
__InjectGetNodes_Generated();
|
||||
OnReadyAfterGetNode();
|
||||
}
|
||||
|
||||
private void OnReadyAfterGetNode()
|
||||
{
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当未显式填写路径时,生成器会默认将字段名推导为唯一名路径:
|
||||
|
||||
- `_leftContainer` -> `%LeftContainer`
|
||||
- `m_rightContainer` -> `%RightContainer`
|
||||
|
||||
## BindNodeSignal 用法
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
|
||||
public partial class Hud : Control
|
||||
{
|
||||
[GetNode]
|
||||
private Button _startButton = null!;
|
||||
|
||||
[GetNode]
|
||||
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()
|
||||
{
|
||||
__InjectGetNodes_Generated();
|
||||
__BindNodeSignals_Generated();
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
__UnbindNodeSignals_Generated();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
生成器会产出两个辅助方法:
|
||||
|
||||
- `__BindNodeSignals_Generated()`:负责统一订阅事件
|
||||
- `__UnbindNodeSignals_Generated()`:负责统一解绑事件
|
||||
|
||||
当前设计只处理 CLR event 形式的 Godot 事件绑定,不会自动调用 `Connect()` / `Disconnect()`。
|
||||
- 你在确认 Godot 运行时 Scene / UI / 存储 / 设置的默认实现:
|
||||
- 先看 `GFramework.Godot`
|
||||
- 你只需要 `Game` 契约,不需要 Godot 宿主或生成器:
|
||||
- 先看 `GFramework.Game` 或 `GFramework.Game.Abstractions`
|
||||
- 你在确认项目接线顺序,而不是单个生成器契约:
|
||||
- 先看 `docs/zh-CN/tutorials/godot-integration.md`
|
||||
|
||||
@ -1,19 +1,161 @@
|
||||
# GFramework.Godot
|
||||
|
||||
GFramework 框架的 Godot 引擎集成模块,提供Godot特定的功能和扩展。
|
||||
`GFramework.Godot` 是 `GFramework` 在 Godot 宿主侧的运行时适配包。
|
||||
|
||||
## 主要功能
|
||||
它建立在 `GFramework.Game`、`GFramework.Game.Abstractions` 与 `GFramework.Core.Abstractions` 之上,把框架已有的架构、
|
||||
Scene / UI、配置、存储、设置、日志与协程能力接到 `Node`、`SceneTree`、`PackedScene`、`FileAccess` 与 `AudioServer`
|
||||
等 Godot 运行时对象上。
|
||||
|
||||
- **Extensions** - Godot节点扩展方法,简化常见开发任务
|
||||
- **Signal** - 流畅的信号连接API,支持链式调用
|
||||
- **Storage** - Godot文件存储系统,支持虚拟路径
|
||||
- **Settings** - Godot设置系统,管理音频和图形设置
|
||||
如果你需要的是 `[GetNode]`、`[BindNodeSignal]`、`AutoLoads` 或 `InputActions` 这类编译期能力,请改为同时安装
|
||||
`GFramework.Godot.SourceGenerators`。这些能力不属于本包。
|
||||
|
||||
## 依赖关系
|
||||
## 包定位
|
||||
|
||||
- 依赖 GFramework.Core
|
||||
- 依赖 GFramework.Core.Abstractions
|
||||
当前包解决的是 Godot 运行时接线,而不是重新定义一套 Godot 专属框架:
|
||||
|
||||
## 详细文档
|
||||
- 架构生命周期与场景树绑定:`AbstractArchitecture`、`ArchitectureAnchor`
|
||||
- 节点运行时辅助:`WaitUntilReadyAsync()`、`AddChildXAsync()`、`QueueFreeX()`、`UnRegisterWhenNodeExitTree(...)`
|
||||
- Godot 风格的 Scene / UI 工厂与 registry:`GodotSceneFactory`、`GodotUiFactory`
|
||||
- Godot 特化的配置、存储与设置实现:`GodotYamlConfigLoader`、`GodotFileStorage`、`GodotAudioSettings`
|
||||
- 宿主侧辅助能力:`Signal(...)` fluent API、Godot 时间源、暂停处理、富文本效果
|
||||
|
||||
参见 [docs/zh-CN/godot/](../docs/zh-CN/godot/) 目录下的详细文档。
|
||||
它不负责:
|
||||
|
||||
- 自动生成节点字段注入代码
|
||||
- 自动生成 `_Ready()` / `_ExitTree()` 接线
|
||||
- 自动扫描所有场景或页面并完成统一注册
|
||||
- 提供 `GodotSceneRouter` 或 `GodotUiRouter` 这类额外 router 类型
|
||||
|
||||
## 与相邻包的关系
|
||||
|
||||
- `GFramework.Game`
|
||||
- 提供 Scene / UI / 配置 / 数据等默认运行时契约与基类。
|
||||
- `GFramework.Godot` 负责把这些能力落到 Godot 宿主。
|
||||
- `GFramework.Game.Abstractions`
|
||||
- 提供 `ISceneFactory`、`IUiFactory`、设置与配置相关契约。
|
||||
- 本包的大部分工厂和适配层都实现这些接口。
|
||||
- `GFramework.Core.Abstractions`
|
||||
- 提供架构、日志、环境等基础契约。
|
||||
- `AbstractArchitecture` 与日志 provider 都建立在这层之上。
|
||||
- `GFramework.Godot.SourceGenerators`
|
||||
- 提供 `project.godot` 元数据、`[GetNode]`、`[BindNodeSignal]`、`[AutoScene]`、`[AutoUiPage]` 等编译期样板生成。
|
||||
- 推荐与本包配套使用,但职责边界要分开理解。
|
||||
|
||||
## 子系统地图
|
||||
|
||||
### `Architectures/`
|
||||
|
||||
- `AbstractArchitecture`
|
||||
- `AbstractGodotModule`
|
||||
- `ArchitectureAnchor`
|
||||
- `IGodotModule`
|
||||
|
||||
用于把架构生命周期绑定到 `SceneTree`,并在需要时把 Godot 模块挂到场景树。
|
||||
|
||||
### `Scene/` 与 `UI/`
|
||||
|
||||
- `GodotSceneFactory`、`GodotSceneRegistry`
|
||||
- `GodotUiFactory`、`GodotUiRegistry`
|
||||
- `SceneBehaviorFactory`、`UiPageBehaviorFactory`
|
||||
|
||||
这部分负责把 `PackedScene`、`Control`、`CanvasLayer` 等 Godot 对象接入 `GFramework.Game` 的 Scene / UI 契约。
|
||||
|
||||
### `Config/`、`Storage/` 与 `Setting/`
|
||||
|
||||
- `GodotYamlConfigLoader`
|
||||
- `GodotFileStorage`
|
||||
- `GodotAudioSettings`、`GodotGraphicsSettings`、`GodotLocalizationSettings`
|
||||
|
||||
这部分解决的是 Godot 文件系统、音频总线、图形与本地化设置等宿主差异。
|
||||
|
||||
### `Extensions/`、`Coroutine/`、`Logging/`、`Pause/`、`Text/`、`Pool/`
|
||||
|
||||
- 节点扩展与 `Signal(...)` fluent API
|
||||
- `GodotTimeSource` 与协程时间分段
|
||||
- Godot 日志 provider
|
||||
- 暂停处理、节点池与富文本效果支持
|
||||
|
||||
这些目录都是“宿主适配层”,不是新的 gameplay 抽象层。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 先区分运行时包和生成器包
|
||||
|
||||
如果你只需要 Godot 运行时适配:
|
||||
|
||||
```bash
|
||||
dotnet add package GeWuYou.GFramework
|
||||
dotnet add package GeWuYou.GFramework.Godot
|
||||
```
|
||||
|
||||
如果你还需要 `project.godot` 强类型入口、节点字段注入和信号绑定,再额外安装:
|
||||
|
||||
```bash
|
||||
dotnet add package GeWuYou.GFramework.Core.SourceGenerators
|
||||
dotnet add package GeWuYou.GFramework.Godot.SourceGenerators
|
||||
```
|
||||
|
||||
### 2. 保持原有架构注册方式,只把宿主协作接到 Godot
|
||||
|
||||
常规模块继续使用 `InstallModule(...)`。
|
||||
|
||||
只有模块自身暴露 `Node`、需要挂到 `ArchitectureAnchor`,或要在 `OnAttach(...)` / `OnDetach()` 里处理 Godot 生命周期副作用时,
|
||||
再使用 `InstallGodotModule(...)`。
|
||||
|
||||
`GFramework.Godot.Tests/Architectures/AbstractArchitectureModuleInstallationTests.cs` 已覆盖一个关键边界:锚点缺失时会先抛
|
||||
`InvalidOperationException`,不会继续执行模块安装。
|
||||
|
||||
### 3. Scene / UI 继续沿用 `Game` 契约
|
||||
|
||||
当前真实边界是:
|
||||
|
||||
- 没有 `GodotSceneRouter`
|
||||
- 没有 `GodotUiRouter`
|
||||
- `GodotSceneFactory` 在 provider 缺失时回退到 `SceneBehaviorFactory`
|
||||
- `GodotUiFactory` 仍要求 `IUiPageBehaviorProvider`
|
||||
|
||||
也就是说,项目通常仍然继承 `GFramework.Game.Scene.SceneRouterBase` 与 `GFramework.Game.UI.UiRouterBase`,只是把工厂和行为落到
|
||||
Godot 上。
|
||||
|
||||
### 4. 按需接入配置、存储和设置
|
||||
|
||||
当项目已经使用 `Game` family 的配置、存储、设置契约时,再补 Godot 侧实现:
|
||||
|
||||
- 配置:`GodotYamlConfigLoader`
|
||||
- 存储:`GodotFileStorage`
|
||||
- 设置:`GodotAudioSettings`、`GodotGraphicsSettings`、`GodotLocalizationSettings`
|
||||
|
||||
不要把这些宿主实现误写成 `Game` family 的默认行为。
|
||||
|
||||
## `ai-libs/` 里的参考接入线索
|
||||
|
||||
`ai-libs/CoreGrid` 仍是当前最直接的消费者证据来源:
|
||||
|
||||
- 架构侧保持普通模块注册,再按需挂接 Godot 宿主
|
||||
- `project.godot` 元数据与节点样板交给 `GFramework.Godot.SourceGenerators`
|
||||
- Scene / UI 继续沿用 `Game` family 的 router 语义
|
||||
|
||||
当 `ai-libs/` 与源码或测试冲突时,应以当前源码与测试为准。
|
||||
|
||||
## 文档入口
|
||||
|
||||
- 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)
|
||||
|
||||
## 什么时候不该把它当成主入口
|
||||
|
||||
以下场景更适合先回到其他包:
|
||||
|
||||
- 只需要 Scene / UI / 配置契约,不需要 Godot 宿主:
|
||||
- 选 `GFramework.Game.Abstractions`
|
||||
- 需要默认运行时实现,但暂时不接 Godot:
|
||||
- 选 `GFramework.Game`
|
||||
- 需要的是 `project.godot` 元数据、节点字段注入或编译期样板:
|
||||
- 选 `GFramework.Godot.SourceGenerators`
|
||||
|
||||
@ -6,6 +6,32 @@ namespace GFramework.SourceGenerators.Tests.Config;
|
||||
[TestFixture]
|
||||
public class SchemaConfigGeneratorTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证 AdditionalFiles 读取被取消时会向上传播取消,而不是伪造成 schema 诊断。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Propagate_Cancellation_When_AdditionalText_Read_Is_Cancelled()
|
||||
{
|
||||
var method = typeof(global::GFramework.Game.SourceGenerators.Config.SchemaConfigGenerator)
|
||||
.GetMethod(
|
||||
"TryReadSchemaText",
|
||||
global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static);
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
var invocationArguments = new object?[]
|
||||
{
|
||||
new ThrowingAdditionalText("monster.schema.json"),
|
||||
cancellationTokenSource.Token,
|
||||
null,
|
||||
null
|
||||
};
|
||||
|
||||
var exception = Assert.Throws<global::System.Reflection.TargetInvocationException>(() =>
|
||||
method!.Invoke(null, invocationArguments));
|
||||
|
||||
Assert.That(exception!.InnerException, Is.TypeOf<OperationCanceledException>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证缺失必填 id 字段时会产生命名明确的诊断。
|
||||
/// </summary>
|
||||
@ -46,6 +72,111 @@ public class SchemaConfigGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证根节点 <c>type</c> 元数据不是字符串时,会返回根对象约束诊断,而不是抛出 JSON 访问异常。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Report_Diagnostic_When_Root_Type_Metadata_Is_Not_A_String()
|
||||
{
|
||||
const string source = """
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class Dummy
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string schema = """
|
||||
{
|
||||
"type": 123,
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = SchemaGeneratorTestDriver.Run(
|
||||
source,
|
||||
("monster.schema.json", schema));
|
||||
|
||||
var diagnostic = result.Results.Single().Diagnostics.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_002"));
|
||||
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("monster.schema.json"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 schema 文件名若生成无效根类型标识符时,会在生成前产生命名明确的诊断。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Report_Diagnostic_When_Schema_File_Name_Generates_Invalid_Root_Type_Identifier()
|
||||
{
|
||||
const string source = """
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class Dummy
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string schema = """
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"name": { "type": "string" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = SchemaGeneratorTestDriver.Run(
|
||||
source,
|
||||
("123-monster.schema.json", schema));
|
||||
|
||||
var diagnostic = result.Results.Single().Diagnostics.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_006"));
|
||||
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("<root>"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("123-monster"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("123MonsterConfig"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于模拟 AdditionalFiles 读取阶段直接收到取消请求的测试桩。
|
||||
/// </summary>
|
||||
private sealed class ThrowingAdditionalText : AdditionalText
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个在读取时抛出取消异常的 AdditionalText。
|
||||
/// </summary>
|
||||
/// <param name="path">虚拟 schema 路径。</param>
|
||||
public ThrowingAdditionalText(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Path { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override SourceText GetText(CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new OperationCanceledException(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证空字符串 <c>const</c> 不会在生成 XML 文档时被当成“缺失约束”跳过。
|
||||
/// </summary>
|
||||
@ -1844,6 +1975,49 @@ public class SchemaConfigGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证同一对象内不同 schema key 若归一化后映射到同一属性名,会在生成前直接给出冲突诊断。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Report_Diagnostic_When_Schema_Keys_Collide_After_Identifier_Normalization()
|
||||
{
|
||||
const string source = """
|
||||
namespace TestApp
|
||||
{
|
||||
public sealed class Dummy
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string schema = """
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"foo-bar": { "type": "string" },
|
||||
"foo_bar": { "type": "string" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var result = SchemaGeneratorTestDriver.Run(
|
||||
source,
|
||||
("monster.schema.json", schema));
|
||||
|
||||
var diagnostic = result.Results.Single().Diagnostics.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_014"));
|
||||
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("foo_bar"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("FooBar"));
|
||||
Assert.That(diagnostic.GetMessage(), Does.Contain("foo-bar"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 schema 顶层允许通过元数据覆盖默认配置目录,并会统一路径分隔符。
|
||||
/// </summary>
|
||||
@ -2299,7 +2473,7 @@ public class SchemaConfigGeneratorTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证引用元数据成员名在不同路径规范化后发生碰撞时,生成器仍会分配全局唯一的成员名。
|
||||
/// 验证引用元数据成员名在不同合法字段路径规范化后发生碰撞时,生成器仍会分配全局唯一的成员名。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names()
|
||||
@ -2360,12 +2534,21 @@ public class SchemaConfigGeneratorTests
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"drop-items": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"x-gframework-ref-table": "item"
|
||||
"drop": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"x-gframework-ref-table": "item"
|
||||
},
|
||||
"items1": {
|
||||
"type": "string",
|
||||
"x-gframework-ref-table": "item"
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop_items": {
|
||||
"dropItems": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"x-gframework-ref-table": "item"
|
||||
@ -2394,6 +2577,7 @@ public class SchemaConfigGeneratorTests
|
||||
Assert.That(generatedSources.TryGetValue("MonsterConfigBindings.g.cs", out var bindingsSource), Is.True);
|
||||
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems ="));
|
||||
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems1 ="));
|
||||
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems2 ="));
|
||||
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems11 ="));
|
||||
}
|
||||
|
||||
@ -2637,6 +2821,12 @@ public class SchemaConfigGeneratorTests
|
||||
Assert.That(catalogSource,
|
||||
Does.Contain(
|
||||
"public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }"));
|
||||
Assert.That(catalogSource,
|
||||
Does.Contain(
|
||||
"using <c>global::System.Collections.Generic.IEqualityComparer<string>?</c> when aggregate registration runs."));
|
||||
Assert.That(catalogSource,
|
||||
Does.Contain(
|
||||
"using <c>global::System.Collections.Generic.IEqualityComparer<int>?</c> when aggregate registration runs."));
|
||||
Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);"));
|
||||
Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options"));
|
||||
Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(effectiveOptions.ItemComparer);"));
|
||||
|
||||
@ -20,6 +20,23 @@ public static class SchemaGeneratorTestDriver
|
||||
public static GeneratorDriverRunResult Run(
|
||||
string source,
|
||||
params (string path, string content)[] additionalFiles)
|
||||
{
|
||||
return Run(
|
||||
source,
|
||||
additionalFiles
|
||||
.Select(static item => (AdditionalText)new InMemoryAdditionalText(item.path, item.content))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行 schema 配置生成器,并允许测试自定义 AdditionalText 行为。
|
||||
/// </summary>
|
||||
/// <param name="source">测试用源码。</param>
|
||||
/// <param name="additionalTexts">自定义 AdditionalText 集合。</param>
|
||||
/// <returns>生成器运行结果。</returns>
|
||||
public static GeneratorDriverRunResult Run(
|
||||
string source,
|
||||
params AdditionalText[] additionalTexts)
|
||||
{
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
||||
var compilation = CSharpCompilation.Create(
|
||||
@ -28,13 +45,9 @@ public static class SchemaGeneratorTestDriver
|
||||
GetMetadataReferences(),
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
|
||||
var additionalTexts = additionalFiles
|
||||
.Select(static item => (AdditionalText)new InMemoryAdditionalText(item.path, item.content))
|
||||
.ToImmutableArray();
|
||||
|
||||
GeneratorDriver driver = CSharpGeneratorDriver.Create(
|
||||
generators: new[] { new SchemaConfigGenerator().AsSourceGenerator() },
|
||||
additionalTexts: additionalTexts,
|
||||
additionalTexts: additionalTexts.ToImmutableArray(),
|
||||
parseOptions: (CSharpParseOptions)syntaxTree.Options);
|
||||
|
||||
driver = driver.RunGenerators(compilation);
|
||||
|
||||
@ -236,7 +236,7 @@ public sealed class GeneratedConfigRegistrationOptions
|
||||
public global::System.Predicate<GeneratedConfigCatalog.TableMetadata>? TableFilter { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable(global::GFramework.Game.Config.YamlConfigLoader, global::System.Collections.Generic.IEqualityComparer<int>?) when aggregate registration runs.
|
||||
/// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable using <c>global::System.Collections.Generic.IEqualityComparer<int>?</c> when aggregate registration runs.
|
||||
/// </summary>
|
||||
public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }
|
||||
}
|
||||
|
||||
@ -1375,6 +1375,197 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 handler 合同里出现未解析错误类型时,生成器会改为运行时精确查找该类型,
|
||||
/// 而不会把无效类型名直接写进生成代码中的 <c>typeof(...)</c>。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public interface IServiceCollection { }
|
||||
|
||||
public static class ServiceCollectionServiceExtensions
|
||||
{
|
||||
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Debug(string msg);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs.Abstractions.Cqrs
|
||||
{
|
||||
public interface IRequest<TResponse> { }
|
||||
public interface INotification { }
|
||||
public interface IStreamRequest<TResponse> { }
|
||||
|
||||
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
|
||||
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
|
||||
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs
|
||||
{
|
||||
public interface ICqrsHandlerRegistry
|
||||
{
|
||||
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CqrsHandlerRegistryAttribute : Attribute
|
||||
{
|
||||
public CqrsHandlerRegistryAttribute(Type registryType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public sealed class CqrsReflectionFallbackAttribute : Attribute
|
||||
{
|
||||
public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
public sealed record BrokenRequest() : IRequest<MissingResponse>;
|
||||
|
||||
public sealed class BrokenHandler : IRequestHandler<BrokenRequest, MissingResponse>
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var execution = ExecuteGenerator(source);
|
||||
var inputCompilationErrors = execution.InputCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatorErrors = execution.GeneratorDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS0246"));
|
||||
Assert.That(generatedCompilationErrors, Is.Empty);
|
||||
Assert.That(generatorErrors, Is.Empty);
|
||||
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
|
||||
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
|
||||
var generatedSource = execution.GeneratedSources[0].content;
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain("registryAssembly.GetType(\"MissingResponse\", throwOnError: false, ignoreCase: false);"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry"));
|
||||
Assert.That(generatedSource, Does.Not.Contain("typeof(MissingResponse)"));
|
||||
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute("));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 <see langword="dynamic" /> 响应类型会在生成阶段归一化为 <see cref="System.Object" />,
|
||||
/// 避免注册器发射非法的 <c>typeof(dynamic)</c>。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Emits_Object_Type_Reference_When_Handler_Response_Uses_Dynamic()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public interface IServiceCollection { }
|
||||
|
||||
public static class ServiceCollectionServiceExtensions
|
||||
{
|
||||
public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Logging
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
void Debug(string msg);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs.Abstractions.Cqrs
|
||||
{
|
||||
public interface IRequest<TResponse> { }
|
||||
public interface INotification { }
|
||||
public interface IStreamRequest<TResponse> { }
|
||||
|
||||
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
|
||||
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
|
||||
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
|
||||
}
|
||||
|
||||
namespace GFramework.Cqrs
|
||||
{
|
||||
public interface ICqrsHandlerRegistry
|
||||
{
|
||||
void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger);
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CqrsHandlerRegistryAttribute : Attribute
|
||||
{
|
||||
public CqrsHandlerRegistryAttribute(Type registryType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
public sealed record DynamicRequest() : IRequest<dynamic>;
|
||||
|
||||
public sealed class DynamicHandler : IRequestHandler<DynamicRequest, dynamic>
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var execution = ExecuteGenerator(source);
|
||||
var inputCompilationErrors = execution.InputCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatorErrors = execution.GeneratorDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(inputCompilationErrors.Select(static diagnostic => diagnostic.Id), Does.Contain("CS1966"));
|
||||
Assert.That(generatedCompilationErrors, Is.Empty);
|
||||
Assert.That(generatorErrors, Is.Empty);
|
||||
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
|
||||
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
|
||||
var generatedSource = execution.GeneratedSources[0].content;
|
||||
Assert.That(generatedSource, Does.Contain("typeof(global::System.Object)"));
|
||||
Assert.That(generatedSource, Does.Not.Contain("typeof(dynamic)"));
|
||||
Assert.That(generatedSource, Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 fallback metadata 仍然必需且 runtime 提供了承载契约时,
|
||||
/// 生成器会继续产出注册器并发射程序集级 <c>CqrsReflectionFallbackAttribute</c>。
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GFramework.Core.SourceGenerators.Rule;
|
||||
using GFramework.SourceGenerators.Tests.Core;
|
||||
|
||||
@ -11,6 +13,60 @@ namespace GFramework.SourceGenerators.Tests.Rule;
|
||||
[TestFixture]
|
||||
public class ContextAwareGeneratorSnapshotTests
|
||||
{
|
||||
private const string SharedContextAwareInfrastructure = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ContextAwareAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware
|
||||
{
|
||||
void SetContext(
|
||||
GFramework.Core.Abstractions.Architectures.IArchitectureContext context);
|
||||
|
||||
GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext();
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitectureContext { }
|
||||
|
||||
public interface IArchitectureContextProvider
|
||||
{
|
||||
IArchitectureContext GetContext();
|
||||
bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Architectures
|
||||
{
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
|
||||
public sealed class GameContextProvider : IArchitectureContextProvider
|
||||
{
|
||||
public IArchitectureContext GetContext() => null;
|
||||
public bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext
|
||||
{
|
||||
context = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
private const string GameContextHelperSource = """
|
||||
|
||||
public static class GameContext
|
||||
{
|
||||
public static IArchitectureContext GetFirstArchitectureContext() => null;
|
||||
}
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 测试ContextAwareGenerator源代码生成器的快照功能
|
||||
/// 验证生成器对带有ContextAware特性的类的处理结果
|
||||
@ -19,76 +75,119 @@ public class ContextAwareGeneratorSnapshotTests
|
||||
[Test]
|
||||
public async Task Snapshot_ContextAwareGenerator()
|
||||
{
|
||||
// 定义测试用的源代码,包含ContextAware特性和相关接口定义
|
||||
const string source = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.Core.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class ContextAwareAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware
|
||||
{
|
||||
void SetContext(
|
||||
GFramework.Core.Abstractions.Architectures.IArchitectureContext context);
|
||||
|
||||
GFramework.Core.Abstractions.Architectures.IArchitectureContext GetContext();
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitectureContext { }
|
||||
|
||||
public interface IArchitectureContextProvider
|
||||
{
|
||||
IArchitectureContext GetContext();
|
||||
bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Architectures
|
||||
{
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
|
||||
public sealed class GameContextProvider : IArchitectureContextProvider
|
||||
{
|
||||
public IArchitectureContext GetContext() => null;
|
||||
public bool TryGetContext<T>(out T? context) where T : class, IArchitectureContext
|
||||
{
|
||||
context = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GameContext
|
||||
{
|
||||
public static IArchitectureContext GetFirstArchitectureContext() => null;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
|
||||
[ContextAware]
|
||||
public partial class MyRule : IContextAware
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// 执行生成器快照测试,将生成的代码与预期快照进行比较
|
||||
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
|
||||
source,
|
||||
CreateContextAwareTestSource(
|
||||
"""
|
||||
[ContextAware]
|
||||
public partial class MyRule : IContextAware
|
||||
{
|
||||
}
|
||||
""",
|
||||
includeGameContextHelper: true),
|
||||
GetSnapshotFolder());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器在用户 partial 类型已经声明常见上下文字段名时仍能生成可编译代码。
|
||||
/// </summary>
|
||||
/// <returns>异步任务,无返回值。</returns>
|
||||
[Test]
|
||||
public async Task Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions()
|
||||
{
|
||||
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
|
||||
CreateContextAwareTestSource(
|
||||
"""
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
|
||||
[ContextAware]
|
||||
public partial class CollisionProneRule : IContextAware
|
||||
{
|
||||
private readonly string _context = "user-field";
|
||||
private static readonly string _contextProvider = "user-provider";
|
||||
private static readonly object _contextSync = new();
|
||||
private IArchitectureContext? _gFrameworkContextAwareContext;
|
||||
private static IArchitectureContextProvider? _gFrameworkContextAwareProvider;
|
||||
private static readonly object _gFrameworkContextAwareSync = new();
|
||||
}
|
||||
"""),
|
||||
GetSnapshotFolder());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器在基类已经占用自动生成字段名时,也会为派生规则类型分配带后缀的唯一成员名。
|
||||
/// </summary>
|
||||
/// <returns>异步任务,无返回值。</returns>
|
||||
[Test]
|
||||
public async Task Snapshot_ContextAwareGenerator_With_Inherited_Field_Name_Collisions()
|
||||
{
|
||||
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
|
||||
CreateContextAwareTestSource(
|
||||
"""
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
|
||||
public abstract class ContextAwareRuleBase
|
||||
{
|
||||
protected IArchitectureContext? _gFrameworkContextAwareContext;
|
||||
protected static IArchitectureContextProvider? _gFrameworkContextAwareProvider;
|
||||
protected static readonly object _gFrameworkContextAwareSync = new();
|
||||
}
|
||||
|
||||
[ContextAware]
|
||||
public partial class InheritedCollisionRule : ContextAwareRuleBase, IContextAware
|
||||
{
|
||||
}
|
||||
"""),
|
||||
GetSnapshotFolder());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 组装 ContextAwareGenerator 快照测试共用的最小宿主源码,避免每个用例都重复长块样板代码。
|
||||
/// </summary>
|
||||
/// <param name="testTypeDeclarations">放在 <c>TestApp</c> 命名空间内的测试类型声明。</param>
|
||||
/// <param name="includeGameContextHelper">是否额外包含兼容旧快照输入的 <c>GameContext</c> 帮助类型。</param>
|
||||
/// <returns>可直接交给生成器测试驱动的完整源码文本。</returns>
|
||||
private static string CreateContextAwareTestSource(string testTypeDeclarations, bool includeGameContextHelper = false)
|
||||
{
|
||||
var gameContextHelper = includeGameContextHelper ? GameContextHelperSource : string.Empty;
|
||||
var testAppDeclarations = IndentBlock(testTypeDeclarations, 4);
|
||||
|
||||
return string.Concat(
|
||||
SharedContextAwareInfrastructure,
|
||||
gameContextHelper,
|
||||
"""
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
using GFramework.Core.Abstractions.Rule;
|
||||
|
||||
""",
|
||||
testAppDeclarations,
|
||||
"""
|
||||
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为内嵌源码片段补齐缩进,使其能安全插入原始字符串模板中的命名空间块。
|
||||
/// </summary>
|
||||
/// <param name="text">要缩进的源码文本。</param>
|
||||
/// <param name="spaces">每行前要补齐的空格数。</param>
|
||||
/// <returns>已经补齐统一缩进的多行文本。</returns>
|
||||
private static string IndentBlock(string text, int spaces)
|
||||
{
|
||||
var indentation = new string(' ', spaces);
|
||||
return string.Join(
|
||||
Environment.NewLine,
|
||||
text.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Trim()
|
||||
.Split('\n')
|
||||
.Select(line => indentation + line));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将运行时测试目录映射回仓库内已提交的上下文感知生成器快照目录。
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
// <auto-generated/>
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
/// <summary>
|
||||
/// 为当前规则类型补充自动生成的架构上下文访问实现。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
|
||||
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
|
||||
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_gFrameworkContextAwareSync1</c> 协调惰性初始化、provider 切换和显式上下文注入;
|
||||
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
|
||||
/// </remarks>
|
||||
partial class CollisionProneRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
{
|
||||
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext1;
|
||||
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider1;
|
||||
private static readonly object _gFrameworkContextAwareSync1 = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前实例绑定的架构上下文。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该属性会先返回通过 <c>IContextAware.SetContext(...)</c> 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。
|
||||
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
|
||||
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_gFrameworkContextAwareSync1</c> 时安全执行;
|
||||
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。
|
||||
/// </remarks>
|
||||
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
|
||||
{
|
||||
get
|
||||
{
|
||||
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
|
||||
// provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync1 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 ??= new global::GFramework.Core.Architectures.GameContextProvider();
|
||||
_gFrameworkContextAwareContext1 ??= _gFrameworkContextAwareProvider1.GetContext();
|
||||
return _gFrameworkContextAwareContext1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置当前生成类型共享的上下文提供者。
|
||||
/// </summary>
|
||||
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
|
||||
/// <exception cref="global::System.ArgumentNullException">当 <paramref name="provider" /> 为 null 时抛出。</exception>
|
||||
/// <remarks>
|
||||
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
|
||||
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
|
||||
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// </remarks>
|
||||
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
|
||||
{
|
||||
global::System.ArgumentNullException.ThrowIfNull(provider);
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 = provider;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置共享上下文提供者,使后续懒加载回退到默认提供者。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该方法主要用于测试清理或跨用例恢复默认行为。
|
||||
/// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// </remarks>
|
||||
public static void ResetContextProvider()
|
||||
{
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 = null;
|
||||
}
|
||||
}
|
||||
|
||||
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareContext1 = context;
|
||||
}
|
||||
}
|
||||
|
||||
global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
|
||||
{
|
||||
return Context;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
// <auto-generated/>
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
/// <summary>
|
||||
/// 为当前规则类型补充自动生成的架构上下文访问实现。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
|
||||
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
|
||||
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_gFrameworkContextAwareSync1</c> 协调惰性初始化、provider 切换和显式上下文注入;
|
||||
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
|
||||
/// </remarks>
|
||||
partial class InheritedCollisionRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
{
|
||||
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext1;
|
||||
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider1;
|
||||
private static readonly object _gFrameworkContextAwareSync1 = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前实例绑定的架构上下文。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该属性会先返回通过 <c>IContextAware.SetContext(...)</c> 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。
|
||||
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
|
||||
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_gFrameworkContextAwareSync1</c> 时安全执行;
|
||||
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。
|
||||
/// </remarks>
|
||||
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
|
||||
{
|
||||
get
|
||||
{
|
||||
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
|
||||
// provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync1 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 ??= new global::GFramework.Core.Architectures.GameContextProvider();
|
||||
_gFrameworkContextAwareContext1 ??= _gFrameworkContextAwareProvider1.GetContext();
|
||||
return _gFrameworkContextAwareContext1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置当前生成类型共享的上下文提供者。
|
||||
/// </summary>
|
||||
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
|
||||
/// <exception cref="global::System.ArgumentNullException">当 <paramref name="provider" /> 为 null 时抛出。</exception>
|
||||
/// <remarks>
|
||||
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
|
||||
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
|
||||
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// </remarks>
|
||||
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
|
||||
{
|
||||
global::System.ArgumentNullException.ThrowIfNull(provider);
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 = provider;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置共享上下文提供者,使后续懒加载回退到默认提供者。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该方法主要用于测试清理或跨用例恢复默认行为。
|
||||
/// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// </remarks>
|
||||
public static void ResetContextProvider()
|
||||
{
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareProvider1 = null;
|
||||
}
|
||||
}
|
||||
|
||||
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
|
||||
lock (_gFrameworkContextAwareSync1)
|
||||
{
|
||||
_gFrameworkContextAwareContext1 = context;
|
||||
}
|
||||
}
|
||||
|
||||
global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
|
||||
{
|
||||
return Context;
|
||||
}
|
||||
|
||||
}
|
||||
@ -10,14 +10,14 @@ namespace TestApp;
|
||||
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
|
||||
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
|
||||
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_contextSync</c> 协调惰性初始化、provider 切换和显式上下文注入;
|
||||
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_gFrameworkContextAwareSync</c> 协调惰性初始化、provider 切换和显式上下文注入;
|
||||
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
|
||||
/// </remarks>
|
||||
partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
{
|
||||
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;
|
||||
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider;
|
||||
private static readonly object _contextSync = new();
|
||||
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _gFrameworkContextAwareContext;
|
||||
private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _gFrameworkContextAwareProvider;
|
||||
private static readonly object _gFrameworkContextAwareSync = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前实例绑定的架构上下文。
|
||||
@ -27,26 +27,20 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
|
||||
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_contextSync</c> 时安全执行;
|
||||
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_gFrameworkContextAwareSync</c> 时安全执行;
|
||||
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。
|
||||
/// </remarks>
|
||||
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
|
||||
{
|
||||
get
|
||||
{
|
||||
var context = _context;
|
||||
if (context is not null)
|
||||
{
|
||||
return context;
|
||||
}
|
||||
|
||||
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
|
||||
// provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
|
||||
lock (_contextSync)
|
||||
// provider 的 GetContext() 会在持有 _gFrameworkContextAwareSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
|
||||
lock (_gFrameworkContextAwareSync)
|
||||
{
|
||||
_contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();
|
||||
_context ??= _contextProvider.GetContext();
|
||||
return _context;
|
||||
_gFrameworkContextAwareProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();
|
||||
_gFrameworkContextAwareContext ??= _gFrameworkContextAwareProvider.GetContext();
|
||||
return _gFrameworkContextAwareContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,6 +49,7 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
/// 配置当前生成类型共享的上下文提供者。
|
||||
/// </summary>
|
||||
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
|
||||
/// <exception cref="global::System.ArgumentNullException">当 <paramref name="provider" /> 为 null 时抛出。</exception>
|
||||
/// <remarks>
|
||||
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
|
||||
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
|
||||
@ -62,9 +57,10 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
/// </remarks>
|
||||
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
|
||||
{
|
||||
lock (_contextSync)
|
||||
global::System.ArgumentNullException.ThrowIfNull(provider);
|
||||
lock (_gFrameworkContextAwareSync)
|
||||
{
|
||||
_contextProvider = provider;
|
||||
_gFrameworkContextAwareProvider = provider;
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,18 +74,18 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||
/// </remarks>
|
||||
public static void ResetContextProvider()
|
||||
{
|
||||
lock (_contextSync)
|
||||
lock (_gFrameworkContextAwareSync)
|
||||
{
|
||||
_contextProvider = null;
|
||||
_gFrameworkContextAwareProvider = null;
|
||||
}
|
||||
}
|
||||
|
||||
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||
{
|
||||
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
|
||||
lock (_contextSync)
|
||||
lock (_gFrameworkContextAwareSync)
|
||||
{
|
||||
_context = context;
|
||||
_gFrameworkContextAwareContext = context;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
| `GFramework.Core.SourceGenerators` | Core 侧通用源码生成器与分析器 | [README](GFramework.Core.SourceGenerators/README.md) |
|
||||
| `GFramework.Game.SourceGenerators` | 游戏内容配置 schema 生成器 | [README](GFramework.Game.SourceGenerators/README.md) |
|
||||
| `GFramework.Cqrs.SourceGenerators` | CQRS handler registry 生成器 | [README](GFramework.Cqrs.SourceGenerators/README.md) |
|
||||
| `GFramework.Godot.SourceGenerators` | Godot 场景专用源码生成器 | [README](GFramework.Godot.SourceGenerators/README.md) |
|
||||
| `GFramework.Godot.SourceGenerators` | Godot 项目元数据、节点注入、信号绑定与 Scene/UI 辅助生成器 | [README](GFramework.Godot.SourceGenerators/README.md) |
|
||||
|
||||
## 内部支撑模块
|
||||
|
||||
|
||||
@ -7,17 +7,51 @@
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-015`
|
||||
- 当前阶段:`Phase 15`
|
||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-028`
|
||||
- 当前阶段:`Phase 28`
|
||||
- 当前焦点:
|
||||
- 当前分支 PR #267 的失败测试已通过 `$gframework-pr-review` 与本地整包测试完成复核
|
||||
- 已确认并修复 `AsyncLogAppender.Flush()` 在“后台线程先清空队列”场景下可能超时返回 `false` 的竞态
|
||||
- 已补上稳定回归测试,避免只在整包 `GFramework.Core.Tests` 里偶发暴露的刷新完成信号问题再次回归
|
||||
- 下一轮默认恢复到 `MA0016` 或 `MA0002` 低风险批次;`MA0015` 与 `MA0077` 继续作为尾项顺手吸收
|
||||
- 已完成 `GFramework.Core` 当前 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险收口批次
|
||||
- 已复核 `net10.0` 下的 `MA0158` 基线:`GFramework.Core` / `GFramework.Cqrs` 当前共有 `16` 个 object lock
|
||||
建议点,属于跨 target 兼容性风险,不在本轮直接批量替换
|
||||
- 已完成 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的剩余 `MA0051` 结构拆分,生成输出保持不变
|
||||
- 已完成 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 的 `MA0051` 结构拆分,生成输出保持不变
|
||||
- 已完成 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0006` 低风险收口,schema 关键字比较显式使用
|
||||
`StringComparison.Ordinal`
|
||||
- 已完成 `SchemaConfigGenerator.cs` 的第一批 `MA0051` 结构拆分:schema 入口解析、属性解析、schema 遍历、数组属性解析、
|
||||
约束文档生成与若干生成代码发射 helper 已拆出语义阶段
|
||||
- 已完成当前 PR #269 review follow-up:`CqrsHandlerRegistryGenerator` 按职责拆分为 partial 生成器文件,
|
||||
`ContextAwareGenerator` 已补上字段名去冲突与锁内读取修正,`Option<T>` 补齐 `<remarks>` 契约说明
|
||||
- 已完成当前 PR #269 第二轮 follow-up:恢复 `EasyEvents`、`CollectionExtensions`、`LoggingConfiguration` 与
|
||||
`FilterConfiguration` 的公共 API 兼容形状,并将 analyzer 兼容性处理收敛到局部 pragma
|
||||
- 已完成当前 PR #269 第三轮 follow-up:继续收口 `SchemaConfigGenerator` 的根类型标识符校验与 XML 文档转义,
|
||||
并补齐 `LoggingConfigurationTests`、`CollectionExtensionsTests`、`Cqrs` helper 抽取与 `ai-plan` 命令文本修正
|
||||
- 已完成当前 PR #269 第四轮 follow-up:将 `CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用改为
|
||||
运行时精确查找路径,并为 `SchemaConfigGenerator` 补上根 `type` 非字符串时的防御与回归测试
|
||||
- 已完成当前 PR #269 第五轮 follow-up:`SchemaConfigGenerator` 补上归一化后属性名冲突诊断并新增
|
||||
`GF_ConfigSchema_014`,`CqrsHandlerRegistryGenerator` 将 `dynamic` 归一化为 `global::System.Object`,
|
||||
同时收紧相关 generator regression tests
|
||||
- 已完成当前 PR #269 failed-test follow-up:修正 `SchemaConfigGeneratorTests`
|
||||
`Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names` 的测试输入,使其继续覆盖
|
||||
reference metadata 成员名全局去冲突,但不再依赖现已被 `GF_ConfigSchema_014` 拦截的非法同层 schema key 冲突
|
||||
- 已完成当前 PR #269 Greptile follow-up:`ContextAwareGenerator` 现在会把基类链显式成员名也纳入
|
||||
`_gFrameworkContextAware*` 字段分配冲突检测,并新增 inherited-field collision 快照回归测试
|
||||
- 已完成当前分支与 `main` 的 `CqrsHandlerRegistryGenerator.cs` 文件级冲突收口:确认 `main` 侧新增的是
|
||||
`OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档,现已按当前 partial 拆分结构迁移到
|
||||
`CqrsHandlerRegistryGenerator.Models.cs`,不回退已完成的生成器拆分
|
||||
- 已更新 `AGENTS.md`:变更模块必须运行对应 `dotnet build -c Release`,并处理或显式报告模块构建 warning,
|
||||
不再默认留给长期 warning 清理分支
|
||||
- `CoroutineScheduler` 的 tag/group 字典已显式使用 `StringComparer.Ordinal`,保持既有区分大小写语义
|
||||
- `EasyEvents.AddEvent<T>()` 的重复注册路径已恢复为 `ArgumentException`,以保持既有异常契约
|
||||
- `Option<T>` 已声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 契约对齐
|
||||
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `0` 条
|
||||
- 当前 `GFramework.Core.SourceGenerators` warnings-only 基线已降到 `0` 条
|
||||
- 当前 `GFramework.Cqrs.SourceGenerators` warnings-only 基线已降到 `0` 条
|
||||
- 当前 `GFramework.Game.SourceGenerators` warnings-only 基线已从 `46` 条降到 `9` 条,剩余均为
|
||||
`SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- `GFramework.Godot` 的 `Timing.cs` 已同步适配新事件签名,但当前 worktree 的 Godot restore 资产仍受 Windows fallback package folder 干扰,独立 build 需在修复资产后补跑
|
||||
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
|
||||
- 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015` 与 `MA0077`
|
||||
只是当前最明显的低数量示例,不构成限定
|
||||
- 下一轮默认继续拆分 `GFramework.Game.SourceGenerators` 的 `MA0051` 热点,或评估跨 target 的 `MA0158`
|
||||
锁替换风险
|
||||
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
|
||||
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
|
||||
|
||||
@ -34,8 +68,24 @@
|
||||
`PhaseChanged` / `CoroutineExceptionEventArgs` XML 文档、`PhaseChanged` 迁移说明和 `ai-plan` 基线注释
|
||||
- 已完成当前 PR #267 failed-test follow-up:修复 `AsyncLogAppender.Flush()` 在队列已被后台线程提前清空时仍可能
|
||||
等待满默认超时并返回 `false` 的竞态,并通过整包 `GFramework.Core.Tests` 重新验证
|
||||
- 当前 `GFramework.Core` `net8.0` warnings-only 基线已降到 `9` 条;剩余 warning 集中在
|
||||
`MA0016` 集合抽象接口、`MA0002` comparer 重载,以及 `MA0015` / `MA0077` 两个低数量尾项
|
||||
- 已完成当前 `GFramework.Core` `net8.0` 剩余低风险 analyzer warning 批次;warnings-only 基线已降到 `0` 条
|
||||
- 已完成 `GFramework.Core.SourceGenerators` 中 `ContextAwareGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条
|
||||
- 已完成 `GFramework.Cqrs.SourceGenerators` 中 `CqrsHandlerRegistryGenerator` 的剩余 `MA0051` 收口;warnings-only 基线已降到 `0` 条
|
||||
- 已完成当前 PR #269 的 review follow-up:收口 `ContextAwareGenerator` 的字段命名冲突 / 锁内读取契约、
|
||||
`CqrsHandlerRegistryGenerator` 的运行时类型 null 防御与超大文件拆分、`SchemaConfigGenerator` 的取消语义,
|
||||
并恢复 `EasyEvents` / `CollectionExtensions` / logging 配置模型的公共 API 兼容形状
|
||||
- 已完成当前 PR #269 的第四轮 review follow-up:确认 5 个 latest-head 未解决线程中仅剩 2 个本地仍成立,
|
||||
已分别在 `CqrsHandlerRegistryGenerator` 与 `SchemaConfigGenerator` 中收口,并补齐定向 generator regression tests
|
||||
- 已完成当前 PR #269 的第五轮 review follow-up:收口 `SchemaConfigGenerator` 的归一化字段名冲突诊断、
|
||||
`CqrsHandlerRegistryGenerator` 的 `dynamic` 类型引用风险,并同步更新 `AGENTS.md` 的模块 build / warning 治理规范
|
||||
- 已完成当前 PR #269 的 failed-test follow-up:将 reference metadata 成员名唯一性回归测试改为合法 schema 路径组合,
|
||||
并重新通过定向 generator test
|
||||
- 已完成当前 PR #269 的 Greptile follow-up:修复 `ContextAwareGenerator` 未覆盖基类成员名冲突的问题,并补齐
|
||||
inherited-collision 快照测试
|
||||
- 已完成当前分支与 `main` 的 `CqrsHandlerRegistryGenerator.cs` 冲突化解:保留当前 partial 结构,并把
|
||||
`main` 侧新增的模型文档合并到 `CqrsHandlerRegistryGenerator.Models.cs`
|
||||
- 已完成 `GFramework.Game.SourceGenerators` 中 `SchemaConfigGenerator` 的第一批 `MA0051` 收口;warnings-only 基线剩余 `9` 条
|
||||
`MA0051`
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
@ -66,16 +116,53 @@
|
||||
nitpick comment 后,确认 8 条高信号项中仍成立的是 1 个行为 bug 与 7 个文档/测试/跟踪缺口,并按最小改动收口
|
||||
- `RP-015` 使用 `$gframework-pr-review` 复核 PR #267 的 CTRF 失败测试评论后,确认 `AsyncLogAppender` 仍存在
|
||||
“队列已空但 Flush 仍超时失败”的竞态;该问题在本地整包 `GFramework.Core.Tests` 中可复现,现已修复并补上稳定回归测试
|
||||
- `RP-016` 将 `GFramework.Core` 当前剩余 `MA0016` / `MA0002` / `MA0015` / `MA0077` 低风险批次清零,并用
|
||||
warnings-only build 与 focused tests 验证配置反序列化、集合扩展、事件重复注册、`Option<T>` 相等性和协程 tag/group 语义
|
||||
- `RP-017` 复核 `MA0158` 当前仍是跨 target 锁类型迁移问题,因此先收口单点 `ContextAwareGenerator` `MA0051`,
|
||||
并通过 source generator 项目 build 与 `ContextAwareGeneratorSnapshotTests` 验证生成输出未回归
|
||||
- `RP-018` 暂缓跨 target `MA0158`,转入 `GFramework.Cqrs.SourceGenerators` 的单文件结构性 warning;
|
||||
通过拆分 handler 分析、运行时类型引用构造、注册器源码发射与精确反射注册输出阶段,清空该项目当前 `MA0051`
|
||||
- `RP-019` 转入 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs`,先完成低风险 `MA0006` 批次;
|
||||
通过 schema 类型比较 helper 与显式 `StringComparison.Ordinal` 清空当前项目的 `MA0006`
|
||||
- `RP-020` 继续拆分 `SchemaConfigGenerator.cs` 的 `MA0051` 热点,将当前项目 warnings-only 基线从 `19` 条降到 `9` 条,
|
||||
并用 focused schema generator tests 验证 50 个用例通过
|
||||
- `RP-021` 使用 `$gframework-pr-review` 复核当前分支 PR #269 后,修复仍在本地成立的 4 个项:将
|
||||
`CqrsHandlerRegistryGenerator` 拆分为职责清晰的 partial 文件、为 `ContextAwareGenerator` 生成字段增加稳定前缀并补上
|
||||
`SetContextProvider` 的运行时 null 校验、为 `Option<T>` 补齐 `<remarks>`,并新增字段重名场景的生成器快照测试
|
||||
- `RP-022` 继续复核 PR #269 的 latest-head review threads 与 nitpick,确认仍成立的项包括公共 API 兼容回退、
|
||||
`ContextAwareGenerator` 字段名真正去冲突与锁内读取、`SchemaConfigGenerator` 取消传播、`Cqrs` 运行时类型 null 防御;
|
||||
已补齐对应回归测试与 focused build/test 验证
|
||||
- `RP-023` 继续复核 PR #269 剩余 nitpick/outside-diff 项,确认仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、
|
||||
aggregate registration comparer XML 文档转义、logging / collection 反射测试补强,以及跟踪文档中的
|
||||
`RestoreFallbackFolders=""` 可复制性问题
|
||||
- `RP-024` 使用 `$gframework-pr-review` 继续复核 PR #269 latest-head unresolved threads,确认 `EasyEvents` 异常契约、
|
||||
`SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 快照冲突线程均已在本地收口,仅剩 `Cqrs` error type
|
||||
直接引用与根 schema `type` 非字符串防御仍成立;现已补齐实现与回归测试
|
||||
- `RP-025` 继续复核 PR #269 剩余 outside-diff / nitpick 信号后,确认本地仍成立的是 `SchemaConfigGenerator`
|
||||
的归一化字段名冲突与 `Cqrs` 对 `dynamic` 的直接类型引用;已分别补上诊断、运行时类型归一化与回归测试,
|
||||
并把“变更模块必须运行对应 build 且处理 warning”的治理规则写回 `AGENTS.md`
|
||||
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
|
||||
|
||||
## 当前风险
|
||||
|
||||
- 公共契约兼容风险:剩余 `MA0016` 若直接改公开集合类型,可能波及用户代码
|
||||
- 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试
|
||||
- analyzer 收口回退风险:后续若继续压 `MA0015` / `MA0016`,容易再次把公共 API 收窄成与既有契约不兼容的形状
|
||||
- 缓解措施:优先保留既有公共 API,并将兼容性例外收敛到局部 pragma;继续用反射断言覆盖返回类型、属性类型与异常类型
|
||||
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
|
||||
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
|
||||
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
|
||||
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
|
||||
- net10 专属 warning 风险:`MA0158` 建议使用 `System.Threading.Lock`,但项目多 target 时需要确认兼容边界
|
||||
- 缓解措施:下一轮先按 target framework 与 API 可用性评估,不直接批量替换共享源码中的 `object` lock
|
||||
- source generator warning 外溢风险:运行 `GFramework.SourceGenerators.Tests` 会构建相邻 generator/test 项目并显示既有
|
||||
`GFramework.Game.SourceGenerators` 与测试项目 warning
|
||||
- 缓解措施:继续以被修改 generator 项目的独立 warnings-only build 作为主验收,并用 focused generator test 验证行为
|
||||
- source generator test warning 治理风险:`GFramework.SourceGenerators.Tests` 当前仍有既有 `MA0051` / `MA0004` / `MA0048`
|
||||
warning,本轮 focused test 已通过,但测试项目整包 warning 尚未进入本轮写集
|
||||
- 缓解措施:本轮已在 failed-test follow-up 的定向 `dotnet test` 中再次确认这些 warning 仍为既有基线;后续若继续修改该测试项目,
|
||||
应按新增 `AGENTS.md` 规则先明确 warning 收口范围,再决定是否进入专门清理切片
|
||||
- ContextAware 基类命名隐藏风险:若生成器只看当前类型声明成员,派生规则会重新占用基类已声明的
|
||||
`_gFrameworkContextAware*` 字段名,导致生成成员隐藏继承状态并让快照无法锁定后缀分配行为
|
||||
- 缓解措施:本轮已改为遍历完整 base-type 链收集保留名,并用 inherited collision 快照用例锁定该行为
|
||||
- Godot 资产文件环境风险:当前 worktree 的 `GFramework.Godot` restore/build 仍会命中 Windows fallback package folder
|
||||
- 缓解措施:后续若继续触达 Godot 模块,先用 Linux 侧 restore 资产或 Windows-hosted 构建链刷新该项目,再补跑定向 build
|
||||
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
|
||||
@ -158,12 +245,80 @@
|
||||
- 结果:`15 Passed`,`0 Failed`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --disable-build-servers`
|
||||
- 结果:`1607 Passed`,`0 Failed`
|
||||
- `RP-016` 的验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;当前 `GFramework.Core` `net8.0` analyzer baseline 已清零
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo`
|
||||
- 结果:`112 Passed`,`0 Failed`;测试构建仍会显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning
|
||||
- `RP-017` 的验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`16 Warning(s)`,`0 Error(s)`;当前 `MA0158` 跨 `GFramework.Core` / `GFramework.Cqrs`,本轮只记录基线不批量改锁
|
||||
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;`ContextAwareGenerator.cs` 已不再出现 `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`1 Passed`,`0 Failed`;测试构建仍显示相邻 source generator 和测试项目的既有 analyzer warning
|
||||
- `RP-018` 的验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;`CqrsHandlerRegistryGenerator.cs` 当前 `MA0051` 已清零
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`14 Passed`,`0 Failed`
|
||||
- 说明:该 test project 构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning;本轮关注的
|
||||
`GFramework.Cqrs.SourceGenerators` 独立 build 已清零
|
||||
- `RP-019` 的验证结果:
|
||||
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:通过;刷新 Linux 侧资产以清除 stale Windows fallback package folder
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`19 Warning(s)`,`0 Error(s)`;当前项目输出已不再出现 `MA0006`,剩余均为 `SchemaConfigGenerator.cs` 的
|
||||
`MA0051`
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:通过;刷新 test project 资产以清除 stale Windows fallback package folder
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`50 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集
|
||||
- `RP-020` 的验证结果:
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`9 Warning(s)`,`0 Error(s)`;当前项目剩余 warning 均为 `SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`50 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集
|
||||
- `RP-021` 的验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;拆分后最大单文件已降到 `851` 行,满足仓库 800-1000 行上限
|
||||
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;`ContextAwareGenerator` 的字段命名与 provider 契约修复未引入新的 generator warning
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:先并行运行两条 `dotnet test` 时触发共享输出文件锁冲突;改为串行重跑后 `ContextAwareGeneratorSnapshotTests=2 Passed`、
|
||||
`CqrsHandlerRegistryGeneratorTests=14 Passed`
|
||||
- 说明:失败来自测试宿主并行写入同一 build 输出,不是代码回归;串行重跑后快照新增的字段重名场景和 CQRS 快照均通过
|
||||
- `RP-022` 的验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;`EasyEvents`、`CollectionExtensions` 与 logging 配置模型的兼容性回退未引入新的 `net8.0` 构建错误
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;`ContainingAssembly` null 防御与发射 helper 精简未引入新的构建错误
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051` warning,非本轮新增
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`63 Passed`,`0 Failed`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`38 Passed`,`0 Failed`
|
||||
- `RP-023` 的验证结果:
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051` warning,未新增新的 generator warning
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;并行构建时 `GFramework.SourceGenerators.Common.dll` 复制阶段出现一次 `MSB3026` 重试,随后成功完成
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`63 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍会显示既有 `GFramework.SourceGenerators.Tests` analyzer warning;不属于本轮写集
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`27 Passed`,`0 Failed`
|
||||
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录
|
||||
2. 下一轮优先在 `MA0016` 与 `MA0002` 之间选择低风险批次继续推进,默认先看 `LoggingConfiguration` /
|
||||
`FilterConfiguration` 与 `CollectionExtensions`
|
||||
3. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
|
||||
4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
|
||||
2. 下一轮优先继续拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的剩余 `MA0051`;建议先从
|
||||
`GenerateBindingsClass`、`AppendGeneratedConfigCatalogType` 或对象/条件 schema target 验证方法切入
|
||||
3. 若改回推进 `MA0158`,先设计 `net8.0` / `net9.0` / `net10.0` 多 target 条件编译方案,不直接批量替换共享源码中的
|
||||
`object` lock
|
||||
4. 若后续继续改动 `GFramework.Godot`,先修复该项目的 Linux 侧 restore 资产,再补跑独立 build
|
||||
5. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
|
||||
|
||||
@ -1,5 +1,403 @@
|
||||
# Analyzer Warning Reduction 追踪
|
||||
|
||||
## 2026-04-23 — RP-028
|
||||
|
||||
### 阶段:`CqrsHandlerRegistryGenerator.cs` 文件级冲突化解(RP-028)
|
||||
|
||||
- 启动复核:
|
||||
- 用户指出当前分支与 `main` 在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs`
|
||||
存在冲突,需要人工确认并解决
|
||||
- 本地检查后确认工作树没有 `UU` 或冲突标记;进一步对比 `origin/main` 发现冲突根因不是运行逻辑回退,而是
|
||||
`main` 在旧的单文件版本里新增了 `OrderedRegistrationKind` / `RuntimeTypeReferenceSpec` 的 XML 文档,
|
||||
而当前分支已将这些类型拆分到 `CqrsHandlerRegistryGenerator.Models.cs`
|
||||
- 决策:
|
||||
- 保留当前分支已经完成的 partial 拆分,不把模型重新塞回 `CqrsHandlerRegistryGenerator.cs`
|
||||
- 以“迁移 `main` 侧文档意图到拆分后的归属文件”为人工合并策略,避免既回退结构拆分又遗漏 `main` 新增文档
|
||||
- 实施调整:
|
||||
- 将 `OrderedRegistrationKind` 的枚举说明与 `RuntimeTypeReferenceSpec` / `FromDirectReference` /
|
||||
`FromReflectionLookup` / `FromExternalReflectionLookup` / `FromArray` / `FromConstructedGeneric`
|
||||
的 XML 文档迁移到 `CqrsHandlerRegistryGenerator.Models.cs`
|
||||
- 保持 `CqrsHandlerRegistryGenerator.cs` 主文件只承载主生成管线,不引入重复模型定义
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- 下一步建议:
|
||||
- 若后续继续处理分支冲突,优先先判断 `main` 改动是否已在当前 partial 文件集里存在等价归属,再决定是否需要真正 merge/rebase
|
||||
- 若回到 PR #269 收口,可继续抓取最新 unresolved threads 与 CI 状态
|
||||
|
||||
## 2026-04-23 — RP-027
|
||||
|
||||
### 阶段:PR #269 Greptile inherited-member collision follow-up(RP-027)
|
||||
|
||||
- 启动复核:
|
||||
- 根据用户补充,重新核对 `$gframework-pr-review` 抓下来的 `greptile-apps[bot]` unresolved 线程,确认仍有一条
|
||||
`ContextAwareGenerator` 关于 inherited member names 未参与 collision detection 的 P1 评论
|
||||
- 本地读取 `CreateGeneratedContextMemberNames(...)` 后确认当前实现只收集 `symbol.GetMembers()`,确实没有遍历基类链
|
||||
- 决策:
|
||||
- 保持现有 `_gFrameworkContextAware*` 前缀和数字后缀分配规则不变,只把保留名集合扩展为“当前类型 + 基类链显式成员”
|
||||
- 沿用既有 `ContextAwareGeneratorSnapshotTests` 模式,新增 inherited-field collision 快照,而不是只写松散字符串断言
|
||||
- 实施调整:
|
||||
- 为 `ContextAwareGenerator` 新增 `CollectReservedContextMemberNames(...)` helper,遍历完整 `BaseType` 链收集显式成员名
|
||||
- 为 `ContextAwareGeneratorSnapshotTests` 增加 `InheritedCollisionRule` 场景,并抽出公共测试源码 helper,避免重复样板
|
||||
- 新增快照 `InheritedCollisionRule.ContextAware.g.cs`,锁定基类已声明 `_gFrameworkContextAware*` 时生成器会回退到 `...1` 后缀
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator_With_Inherited_Field_Name_Collisions|FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator_With_User_Field_Name_Collisions|FullyQualifiedName~ContextAwareGeneratorSnapshotTests.Snapshot_ContextAwareGenerator" -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`3 Passed`,`0 Failed`
|
||||
- 说明:`GFramework.SourceGenerators.Tests` 仍打印既有 `MA0048`、`MA0051`、`MA0004` warning;本轮未扩大到测试项目 warning 清理
|
||||
- 下一步建议:
|
||||
- 若继续收口 PR #269,可再次抓取最新 unresolved threads,确认 Greptile / CodeRabbit 当前是否只剩陈旧信号
|
||||
- 若继续推进 analyzer 主线,可单独评估 `GFramework.SourceGenerators.Tests` 的 warning 清理是否值得开新切片
|
||||
|
||||
## 2026-04-23 — RP-026
|
||||
|
||||
### 阶段:PR #269 failed-test follow-up(RP-026)
|
||||
|
||||
- 启动复核:
|
||||
- 使用 `$gframework-pr-review` 抓取当前分支 PR #269 的 test report,确认最新失败信号来自
|
||||
`SchemaConfigGeneratorTests.Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names`
|
||||
- 本地复测前先对 `GFramework.SourceGenerators.Tests` 执行 `dotnet restore -p:RestoreFallbackFolders=""`,
|
||||
规避当前 WSL worktree 仍残留的 Windows NuGet fallback package folder 资产干扰
|
||||
- 决策:
|
||||
- 保持 `SchemaConfigGenerator` 当前 `GF_ConfigSchema_014` 语义不变;PR 失败是测试输入陈旧,而不是生成器行为回退
|
||||
- 将用例改写为“合法 schema 路径在 reference metadata member name 上碰撞”的场景,继续覆盖全局唯一后缀分配逻辑
|
||||
- 实施调整:
|
||||
- 将测试 schema 从根级 `drop-items` / `drop_items` 非法同层冲突改为 `drop.items`、`drop.items1`、`dropItems`、
|
||||
`dropItems1` 的合法组合
|
||||
- 更新断言,验证 `MonsterConfigBindings.g.cs` 中继续生成 `DropItems`、`DropItems1`、`DropItems2` 与 `DropItems11`
|
||||
- 验证结果:
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`1 Passed`,`0 Failed`
|
||||
- 说明:`GFramework.SourceGenerators.Tests` 在构建阶段仍会打印既有 `MA0048`、`MA0051`、`MA0004` warning;本轮未扩展到该测试项目的 warning 清理
|
||||
- 下一步建议:
|
||||
- 若继续收口 PR #269,可再次抓取最新 test report / open thread,确认是否还有新的 CI 失败信号
|
||||
- 若回到 analyzer 主线,优先决定是否为 `GFramework.SourceGenerators.Tests` 单独开一轮 warning 清理切片
|
||||
|
||||
## 2026-04-23 — RP-025
|
||||
|
||||
### 阶段:PR #269 第五轮 review follow-up 与模块 build / warning 治理补充(RP-025)
|
||||
|
||||
- 启动复核:
|
||||
- 继续使用 `$gframework-pr-review` 读取 PR #269 当前 latest review、outside-diff comment、nitpick comment 与 open-thread 摘要
|
||||
- 本地核对后确认 `SchemaConfigGenerator` 的取消传播、根 `type` 非字符串防御、`ContextAware` 冲突快照与
|
||||
`Cqrs` error type 线程均已是陈旧信号;仍成立的是归一化字段名冲突与 `dynamic` 运行时类型引用问题
|
||||
- 决策:
|
||||
- `SchemaConfigGenerator` 不复用 `GF_ConfigSchema_006`,改为新增专门的冲突诊断 `GF_ConfigSchema_014`,
|
||||
避免把“标识符非法”和“归一化后重名”混成同一类错误
|
||||
- `CqrsHandlerRegistryGenerator` 对 `dynamic` 采用“生成期归一化为 `global::System.Object`”策略,而不是退回更宽泛的
|
||||
fallback 路径,保持精确注册能力且避免发射 `typeof(dynamic)`
|
||||
- `AGENTS.md` 增加模块级 build / warning 治理规则,要求后续改代码时必须对受影响模块跑 Release build,并处理或显式报告 warning
|
||||
- 实施调整:
|
||||
- 为 `SchemaConfigGenerator` 增加对象级生成属性名登记 helper,在 `ParseObjectSpec(...)` 中拦截 `foo-bar` /
|
||||
`foo_bar` 这类归一化后冲突,并新增 `ConfigSchemaDiagnostics.DuplicateGeneratedIdentifier`
|
||||
- 为 `SchemaConfigGeneratorTests` 补上冲突诊断回归测试;为 `CqrsHandlerRegistryGeneratorTests` 收紧
|
||||
unresolved-type 断言并新增 `dynamic` 类型归一化回归测试
|
||||
- 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 增加 `TypeKind.Dynamic` 归一化处理,并保持
|
||||
`TypeKind.Error` 的保守回退
|
||||
- 为 `AGENTS.md` 补充“受影响模块必须独立 build 且 warning 不能默认甩给长期分支”的硬性规范
|
||||
- 验证结果:
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:通过;并行 restore 时出现一次共享 `obj` 文件已存在的竞争噪音,串行验证后未再复现
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -clp:"Summary;WarningsOnly" -nologo`
|
||||
- 结果:`9 Warning(s)`,`0 Error(s)`;维持既有 `SchemaConfigGenerator.cs` `MA0051` 基线,未新增 warning
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~Run_Should_Report_Diagnostic_When_Schema_Keys_Collide_After_Identifier_Normalization|FullyQualifiedName~Emits_Object_Type_Reference_When_Handler_Response_Uses_Dynamic|FullyQualifiedName~Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types" -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`3 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍打印既有 `MA0051` / `MA0004` / `MA0048` warning,不属于本轮 generator 模块写集,但已在 tracking 风险中记录
|
||||
- 下一步建议:
|
||||
- 若继续收口 PR #269,可再次抓取最新 unresolved threads,确认 GitHub 上剩余 open thread 是否全部转为陈旧信号
|
||||
- 若回到 analyzer 主线,继续推进 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 剩余 `MA0051`
|
||||
|
||||
## 2026-04-22 — RP-024
|
||||
|
||||
### 阶段:PR #269 第四轮 review follow-up 收口(RP-024)
|
||||
|
||||
- 启动复核:
|
||||
- 延续 `$gframework-pr-review` 对 PR #269 latest-head unresolved threads 的复核,重点核对最新 5 个未解决线程是否仍与当前
|
||||
worktree 一致
|
||||
- 本地确认 `EasyEvents` 异常契约、`SchemaConfigGenerator` 取消传播与 `ContextAwareGenerator` 字段冲突线程已是陈旧信号,
|
||||
真正仍成立的仅剩 `CqrsHandlerRegistryGenerator` 的 Roslyn error type 直接引用,以及根 schema `type` 非字符串时的
|
||||
`GetString()` 防御
|
||||
- 决策:
|
||||
- `CqrsHandlerRegistryGenerator` 保持现有“优先精确重建、必要时退回运行时查找”的设计,不引入新的程序集级 fallback 契约分支;
|
||||
只在 `CanReferenceFromGeneratedRegistry(...)` 中显式拒绝 `TypeKind.Error`,让未解析类型走已有运行时查找路径
|
||||
- `SchemaConfigGenerator` 继续沿用现有 `GF_ConfigSchema_002` 诊断,不新增诊断 ID;仅在根对象校验入口补上
|
||||
`JsonValueKind.String` 前置判断
|
||||
- 实施调整:
|
||||
- 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 增加 `TypeKind.Error` 防御,避免把未解析类型写成生成代码里的
|
||||
`typeof(...)`
|
||||
- 为 `SchemaConfigGeneratorTests` 补上根 `type` 为数字时返回 `GF_ConfigSchema_002` 的回归测试
|
||||
- 为 `CqrsHandlerRegistryGeneratorTests` 补上未解析 error type 会改走运行时 `GetType(...)` 精确查找的回归测试
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests.Run_Should_Report_Diagnostic_When_Root_Type_Metadata_Is_Not_A_String|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Runtime_Type_Lookup_When_Handler_Contract_Contains_Unresolved_Error_Types" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`2 Passed`,`0 Failed`
|
||||
- 说明:测试命令需在无沙箱环境下运行,因为当前 test host 在沙箱内创建本地 socket 会收到 `Permission denied`
|
||||
- 下一步建议:
|
||||
- 若继续压缩 PR #269 的 review backlog,可再次抓取最新 unresolved threads,确认 GitHub 上仅剩陈旧线程后再决定是否继续代码改动
|
||||
- 若回到 analyzer 主线,继续推进 `SchemaConfigGenerator.cs` 剩余 `MA0051`
|
||||
|
||||
## 2026-04-22 — RP-023
|
||||
|
||||
### 阶段:PR #269 第三轮 review follow-up 收口(RP-023)
|
||||
|
||||
- 启动复核:
|
||||
- 延续 `$gframework-pr-review` 对 PR #269 的 latest-head unresolved threads、outside-diff comment 与 nitpick comment
|
||||
- 本地核实后确认剩余仍成立的项集中在 `SchemaConfigGenerator` 根类型名校验、aggregate registration comparer XML 文档转义、
|
||||
`LoggingConfigurationTests` / `CollectionExtensionsTests` 断言补强,以及 `ai-plan` 命令文本可复制性
|
||||
- 决策:
|
||||
- `SchemaConfigGenerator` 沿用现有 `InvalidGeneratedIdentifier` 诊断,不新增诊断 ID;将根类型名校验收敛到独立 helper,
|
||||
让顶层 schema 文件名与属性名共享同一类安全边界
|
||||
- aggregate registration comparer 文档直接复用现有 `EscapeXmlDocumentation(...)`,避免在 `///` 注释里再次写入原始泛型尖括号
|
||||
- `CqrsHandlerRegistryGenerator` 的重复反射查找分支采用小 helper 抽取,不改变 fallback 语义和快照输出
|
||||
- 实施调整:
|
||||
- 为 `SchemaConfigGenerator` 新增 `TryBuildRootTypeIdentifiers(...)`,在进入 `ParseObjectSpec(...)` 前拦截非法根类型名
|
||||
- 调整 aggregate registration comparer 属性的 XML 文档,使用 `<c>...</c>` 包裹并转义泛型类型文本
|
||||
- 为 `SchemaConfigGeneratorTests` 增加非法 schema 文件名诊断回归,并补强 generated catalog 中 comparer 文档断言
|
||||
- 为 `LoggingConfigurationTests` 增加正向键存在和值断言,为 `CollectionExtensionsTests` 补齐返回类型泛型参数绑定断言
|
||||
- 为 `CqrsHandlerRegistryGenerator.RuntimeTypeReferences` 抽取共享反射查找 helper,并修正 active tracking 中的转义引号
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;仍保留既有 `9` 条 `SchemaConfigGenerator.cs` `MA0051`
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release --no-restore -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;并行构建时出现一次 `MSB3026` 文件占用重试,自动恢复后完成
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~SchemaConfigGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`63 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍打印既有 source-generator-tests analyzer warning,不属于本轮写集
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`27 Passed`,`0 Failed`
|
||||
- 下一步建议:
|
||||
- 若本轮验证通过,继续回到 `SchemaConfigGenerator.cs` 剩余 `MA0051`
|
||||
- 若 PR #269 仍有未关闭 review thread,再按“先本地复核、再最小修复”的节奏收口
|
||||
|
||||
## 2026-04-22 — RP-022
|
||||
|
||||
### 阶段:PR #269 第二轮 review follow-up 收口(RP-022)
|
||||
|
||||
- 启动复核:
|
||||
- 延续 `$gframework-pr-review` 的 PR #269 结果,继续核对 latest-head unresolved threads 与 nitpick comment
|
||||
- 结合本地实现确认仍成立的项不止第一轮记录的 4 个,还包括公共 API 兼容回退、`SchemaConfigGenerator` 取消传播、
|
||||
`ContextAwareGenerator` 真正的字段名去冲突与锁内读取修正、`Cqrs` 运行时类型 null 防御
|
||||
- 决策:
|
||||
- 对公共 API 兼容项优先保持既有契约,不为了压 analyzer 而继续收窄返回类型、属性类型或异常类型
|
||||
- `ContextAwareGenerator` 采用保守并发修复:移除未加锁 fast-path,统一在锁内读取上下文缓存,并让生成字段名按已有成员去冲突
|
||||
- `SchemaConfigGenerator` 在取消已请求时直接重新抛出 `OperationCanceledException`,避免把取消误报告成普通诊断
|
||||
- 实施调整:
|
||||
- 将 `EasyEvents.AddEvent<T>()` 的重复注册异常恢复为 `ArgumentException`,并在测试中恢复既有异常契约断言
|
||||
- 将 `CollectionExtensions.ToDictionarySafe(...)` 返回类型恢复为 `Dictionary<TKey, TValue>`,并新增反射测试锁定公开 API 形状
|
||||
- 将 `LoggingConfiguration` / `FilterConfiguration` 的公开集合属性恢复为具体 `List<>` / `Dictionary<,>` 类型,
|
||||
并新增反射测试与默认 comparer 语义断言
|
||||
- 为 `CqrsHandlerRegistryGenerator` 的命名类型引用构造补上 `ContainingAssembly is null` 防御,移除发射 helper 冗余布尔参数
|
||||
- 为 `SchemaConfigGenerator` 补上“仅在 cancellationToken 已取消时重抛”的 catch 分支,并为测试驱动添加多 `AdditionalText` 重载
|
||||
- 为 `ContextAwareGenerator` 增加生成成员名分配逻辑,新增 `_gFrameworkContextAware*` 与旧 `_context*` 双冲突快照场景,
|
||||
同时移除 getter 中未加锁 fast-path
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:通过;仍有既有 `9` 条 `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~SchemaConfigGeneratorTests|FullyQualifiedName~ContextAwareGeneratorSnapshotTests|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`63 Passed`,`0 Failed`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~EasyEventsTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests" -m:1 -p:RestoreFallbackFolders="" -v minimal`
|
||||
- 结果:`38 Passed`,`0 Failed`
|
||||
- 下一步建议:
|
||||
- 回到 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的剩余 `MA0051`
|
||||
- 若后续 review 再提 analyzer 兼容建议,先做公共契约回归检查,再决定是否接受该建议
|
||||
|
||||
## 2026-04-22 — RP-021
|
||||
|
||||
### 阶段:PR #269 review follow-up 收口(RP-021)
|
||||
|
||||
- 启动复核:
|
||||
- 使用 `$gframework-pr-review` 读取当前分支 PR #269 的 CodeRabbit outside-diff 与 nitpick 汇总
|
||||
- 本地复核后确认仍成立的 4 个项分别是:`CqrsHandlerRegistryGenerator.cs` 超过仓库文件大小上限、
|
||||
`ContextAwareGenerator` 生成字段名可能与用户 partial 类型冲突、`SetContextProvider` 缺少运行时 null 防御、
|
||||
`Option<T>` 缺少 `<remarks>` 契约说明
|
||||
- 决策:
|
||||
- `CqrsHandlerRegistryGenerator` 继续采用既有 partial helper 风格,按“主流程 / 运行时类型引用 / 源码发射 / 模型”四个文件拆分,
|
||||
保持生成顺序、日志文本、fallback 契约和快照输出不变
|
||||
- `ContextAwareGenerator` 只收口仍成立的 review 项,不引入未被本地证实的 `Volatile.Read/Write` 变更
|
||||
- 为字段命名冲突新增生成器快照场景,避免后续回退到 `_context` / `_contextProvider` / `_contextSync`
|
||||
- 实施调整:
|
||||
- 将 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 拆为 4 个 partial 文件,分别承载主生成管线、
|
||||
runtime type reference 构造、source emission helper 与嵌套 specs/models
|
||||
- 将 `ContextAwareGenerator` 生成字段统一改为 `_gFrameworkContextAware*` 前缀,同步更新 XML 文档、注释和显式接口实现
|
||||
- 为 `SetContextProvider(...)` 增加 `ArgumentNullException.ThrowIfNull(provider)` 与 XML `<exception>` 说明
|
||||
- 为 `Option<T>` 补充 `<remarks>`,明确 `Some/None`、`null` 约束、不可变语义与推荐使用方式
|
||||
- 新增 `CollisionProneRule.ContextAware.g.cs` 快照,覆盖用户字段名与生成字段名冲突场景
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;拆分后 `CqrsHandlerRegistryGenerator` 最大单文件为 `851` 行
|
||||
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`14 Passed`,`0 Failed`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~ContextAwareGeneratorSnapshotTests -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`2 Passed`,`0 Failed`
|
||||
- 说明:最初并行跑两个 `dotnet test` 命令时触发共享输出文件锁冲突;串行重跑后确认是测试宿主环境噪音而非代码回归
|
||||
- 下一步建议:
|
||||
- 若本轮验证通过,可继续回到 `SchemaConfigGenerator` 剩余 `MA0051`
|
||||
- 若 review 再次聚焦 `ContextAwareGenerator` 并发可见性问题,需要先补最小复现测试,再决定是否引入 `Volatile` 语义
|
||||
|
||||
## 2026-04-22 — RP-020
|
||||
|
||||
### 阶段:`SchemaConfigGenerator` 第一批 `MA0051` 结构拆分(RP-020)
|
||||
|
||||
- 启动复核:
|
||||
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
|
||||
- `GFramework.Game.SourceGenerators` warnings-only build 复现 `19` 条 warning,全部为
|
||||
`GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- 决策:
|
||||
- 本轮继续低风险结构拆分,不改变 schema 支持范围、诊断 ID、生成类型形状或输出顺序
|
||||
- 未使用 subagent;critical path 是本地复现 warning、拆分语义阶段并用 focused schema generator tests 验证行为
|
||||
- 实施调整:
|
||||
- 将 schema 入口解析拆为文本读取、root 验证、id key 验证和 `SchemaFileSpec` 构造阶段
|
||||
- 将属性解析拆为共享上下文提取、类型分派、标量/对象/数组属性构造 helper
|
||||
- 将统一 schema 遍历拆为对象属性、dependentSchemas、allOf、条件分支、not、array items / contains 等遍历阶段
|
||||
- 将约束文档生成拆为 const、numeric、string、array、object 约束片段
|
||||
- 将 catalog/registration/YAML/lookup/object type 等生成代码发射路径中的小型高收益 helper 拆出
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`9 Warning(s)`,`0 Error(s)`;当前项目剩余 warning 均为 `SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`50 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示既有 source generator test analyzer warning;不属于本轮写集
|
||||
- 下一步建议:
|
||||
- 继续该主题时,优先拆分 `GenerateBindingsClass`、`AppendGeneratedConfigCatalogType` 或对象/条件 schema target 验证方法
|
||||
- 若转回 `MA0158`,仍需先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock
|
||||
|
||||
## 2026-04-22 — RP-019
|
||||
|
||||
### 阶段:`SchemaConfigGenerator` 当前 `MA0006` 收口(RP-019)
|
||||
|
||||
- 启动复核:
|
||||
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
|
||||
- Windows Git interop 在当前 shell 中返回 WSL socket 错误;本轮使用显式 `--git-dir` / `--work-tree` 读取状态
|
||||
- `GFramework.Game.SourceGenerators` 首次 build 受 stale Windows fallback package folder 影响,刷新 restore 资产后复现
|
||||
`46` 条 warning,其中 `MA0006=27`,其余为 `SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- 决策:
|
||||
- 本轮先收口低风险 `MA0006`,不在同一 slice 中拆分 `SchemaConfigGenerator.cs` 的长方法
|
||||
- 未使用 subagent;critical path 是本地复现 warning、替换 schema 字符串比较并用 focused schema generator tests 验证输出行为
|
||||
- 实施调整:
|
||||
- 为 schema 类型关键字新增 `IsSchemaType` / `IsNumericSchemaType` helper,统一使用 `StringComparison.Ordinal`
|
||||
- 将 id key 类型验证、约束文档生成、required property 文档和路径拼接中的直接字符串比较改为显式 ordinal 比较
|
||||
- 修正 `JsonElement.GetString()` 后的 nullable flow,避免新增 `CS8604`
|
||||
- 验证结果:
|
||||
- `dotnet restore GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:通过
|
||||
- `dotnet build GFramework.Game.SourceGenerators/GFramework.Game.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`19 Warning(s)`,`0 Error(s)`;当前项目输出已无 `MA0006`,剩余均为 `MA0051`
|
||||
- `dotnet restore GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:通过
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~SchemaConfigGenerator -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`50 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示既有 analyzer warning;不属于本轮写集
|
||||
- 下一步建议:
|
||||
- 继续该主题时,优先拆分 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0051`
|
||||
- 若回到 `MA0158`,先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock
|
||||
|
||||
## 2026-04-22 — RP-018
|
||||
|
||||
### 阶段:`CqrsHandlerRegistryGenerator` 剩余 `MA0051` 收口(RP-018)
|
||||
|
||||
- 启动复核:
|
||||
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
|
||||
- `MA0158` 锁迁移仍然跨 `GFramework.Core` / `GFramework.Cqrs` 多 target 共享源码,继续视为需要单独设计的兼容性问题
|
||||
- `GFramework.Cqrs.SourceGenerators` warnings-only build 复现 `CqrsHandlerRegistryGenerator.cs` 的 `6` 个 `MA0051`
|
||||
- 决策:
|
||||
- 本轮暂缓 `MA0158`,转入单文件、可由生成器测试覆盖的 `GFramework.Cqrs.SourceGenerators` 结构拆分
|
||||
- 未使用 subagent;critical path 是本地复现 warning、拆分源码发射流程并用 focused generator tests 验证输出未变
|
||||
- 实施调整:
|
||||
- 将 handler candidate 分析拆为接口收集、候选构造和单接口注册分类阶段
|
||||
- 将运行时类型引用构造拆为已构造泛型、命名类型反射查找等独立 helper
|
||||
- 将注册器源码生成拆为文件头、程序集特性、注册器类型、`Register` 方法和服务注册日志发射 helper
|
||||
- 将有序注册与精确反射注册输出拆为独立阶段,保留原有排序和生成文本形状
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter FullyQualifiedName~CqrsHandlerRegistryGeneratorTests -m:1 -p:RestoreFallbackFolders= -nologo`
|
||||
- 结果:`14 Passed`,`0 Failed`
|
||||
- 说明:测试项目构建仍显示 `GFramework.Game.SourceGenerators` 与测试项目中的既有 analyzer warning;不属于本轮写集
|
||||
- 下一步建议:
|
||||
- 继续该主题时,优先处理 `GFramework.Game.SourceGenerators/Config/SchemaConfigGenerator.cs` 的 `MA0006` 低风险批次
|
||||
- 若回到 `MA0158`,先设计多 target 条件编译方案,再考虑替换共享源码中的 `object` lock
|
||||
|
||||
## 2026-04-22 — RP-017
|
||||
|
||||
### 阶段:`ContextAwareGenerator` 剩余 `MA0051` 收口(RP-017)
|
||||
|
||||
- 启动复核:
|
||||
- 当前 worktree 仍映射到 `analyzer-warning-reduction` active topic
|
||||
- `GFramework.Core` `net10.0` warnings-only build 在刷新 restore fallback 资产后复现 `16` 个 `MA0158`
|
||||
- `GFramework.Core.SourceGenerators` warnings-only build 复现 `ContextAwareGenerator.GenerateContextProperty` 的单个
|
||||
`MA0051`
|
||||
- 决策:
|
||||
- `MA0158` 涉及 `GFramework.Core` 与 `GFramework.Cqrs` 的 object lock 字段,且项目仍多 target 到 `net8.0` / `net9.0`
|
||||
/ `net10.0`,因此本轮不直接批量替换为 `System.Threading.Lock`
|
||||
- 先处理单文件、单 warning、生成输出可由 snapshot 验证的 `ContextAwareGenerator` 结构拆分
|
||||
- 未使用 subagent;本轮 critical path 是本地复现 warning、拆分方法并验证生成输出,拆分后写集只包含单个 generator 文件和
|
||||
active `ai-plan` 文档
|
||||
- 实施调整:
|
||||
- 将 `GenerateContextProperty` 拆为 `GenerateContextBackingFields`、`GenerateContextGetter` 与
|
||||
`GenerateContextProviderConfiguration`
|
||||
- 保留原有 `StringBuilder` 追加顺序与生成代码文本,避免 snapshot 变更
|
||||
- 为新增 helper 补充 XML 注释,说明字段、getter 与 provider 配置 API 的生成职责
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`16 Warning(s)`,`0 Error(s)`;记录当前 `MA0158` 基线,不作为本轮修改范围
|
||||
- `dotnet build GFramework.Core.SourceGenerators/GFramework.Core.SourceGenerators.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`;`ContextAwareGenerator.cs` 的 `MA0051` 已清零
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~ContextAwareGeneratorSnapshotTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
|
||||
- 结果:`1 Passed`,`0 Failed`
|
||||
- 说明:该 test project 构建仍显示相邻 generator/test 项目的既有 analyzer warning;本轮关注的
|
||||
`GFramework.Core.SourceGenerators` 独立 build 已清零
|
||||
- 下一步建议:
|
||||
- 继续该主题时,优先设计 `MA0158` 的多 target 兼容迁移方案;如果风险过高,再单独切入
|
||||
`GFramework.Cqrs.SourceGenerators` 或 `GFramework.Game.SourceGenerators` 的结构性 warning
|
||||
|
||||
## 2026-04-22 — RP-016
|
||||
|
||||
### 阶段:`GFramework.Core` 剩余低风险 warning 批次清零(RP-016)
|
||||
|
||||
- 依据 `RP-015` 的下一步建议,本轮恢复到 `MA0016` / `MA0002` 低风险批次,并顺手吸收仍集中在
|
||||
`GFramework.Core` 的 `MA0015` 与 `MA0077`
|
||||
- 基线复核:
|
||||
- 首次使用 Linux `dotnet` 时仍被当前 worktree 的 Windows fallback package folder restore 资产阻断
|
||||
- 切换到 host Windows `dotnet` 后,`GFramework.Core` `net8.0` warnings-only build 复现 `9` 条 warning:
|
||||
`MA0016=5`、`MA0002=2`、`MA0015=1`、`MA0077=1`
|
||||
- 实施调整:
|
||||
- 将 `LoggingConfiguration.Appenders` / `LoggerLevels` 与 `FilterConfiguration.Namespaces` / `Filters`
|
||||
的公开类型改为集合抽象接口,同时保留 `List<T>` / `Dictionary<TKey,TValue>` 默认实例,兼顾 analyzer 与现有配置消费路径
|
||||
- 将 `CollectionExtensions.ToDictionarySafe(...)` 返回类型改为 `IDictionary<TKey,TValue>`,内部仍使用 `Dictionary<TKey,TValue>`
|
||||
保留“重复键以后值覆盖前值”的实现语义
|
||||
- 为 `CoroutineScheduler` 的 `_tagged` 与 `_grouped` 字典显式指定 `StringComparer.Ordinal`,将原有默认区分大小写语义写入代码
|
||||
- 将 `EasyEvents.AddEvent<T>()` 重复注册失败从 `ArgumentException` 改为 `InvalidOperationException`;该路径表示状态冲突,
|
||||
不是某个方法参数无效,因此不能为 `MA0015` 人造参数名
|
||||
- 为 `Option<T>` 声明 `IEquatable<Option<T>>`,与已有强类型 `Equals(Option<T>)` 实现对齐
|
||||
- 验证结果:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -nologo -clp:"Summary;WarningsOnly"`
|
||||
- 结果:`0 Warning(s)`,`0 Error(s)`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~LoggingConfigurationTests|FullyQualifiedName~ConfigurableLoggerFactoryTests|FullyQualifiedName~CollectionExtensionsTests|FullyQualifiedName~EasyEventsTests|FullyQualifiedName~OptionTests|FullyQualifiedName~CoroutineGroupTests|FullyQualifiedName~CoroutineSchedulerTests" -m:1 -nologo`
|
||||
- 结果:`112 Passed`,`0 Failed`
|
||||
- 说明:测试构建仍显示既有 `net10.0` `MA0158` 与 source generator `MA0051` warning;这些不属于本轮
|
||||
`GFramework.Core` `net8.0` 剩余 warning 批次
|
||||
- 当前结论:
|
||||
- `GFramework.Core` `net8.0` 当前 analyzer warning baseline 已清零
|
||||
- analyzer topic 仍可继续,但下一轮应转入 `net10.0` 专属 `MA0158` 兼容性评估,或单独处理 source generator 剩余
|
||||
`MA0051`
|
||||
- 下一步建议:
|
||||
- 优先评估 `MA0158` 在多 target 源码中的安全推进方式;若风险过高,再处理
|
||||
`GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs` 的结构拆分
|
||||
|
||||
## 2026-04-21 — RP-015
|
||||
|
||||
### 阶段:PR #267 failed-test follow-up 收口(RP-015)
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
### 验证
|
||||
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过;无构建失败,主题满足归档前的最终验证要求
|
||||
|
||||
### 下一步
|
||||
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
# Documentation Full Coverage Governance Validation History Through RP-007
|
||||
|
||||
以下内容从 active tracking 中迁出,用于保留 `DOCUMENTATION-FULL-COVERAGE-GOV-RP-001` 到
|
||||
`DOCUMENTATION-FULL-COVERAGE-GOV-RP-007` 的详细验证历史。默认 `boot` 只需要读取 active tracking 中的最新摘要;
|
||||
若需要追溯早期验证命令与结果,再回到本归档文件。
|
||||
|
||||
## 详细验证历史
|
||||
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/index.md`
|
||||
- 结果:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`
|
||||
- 结果:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`
|
||||
- 结果:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在补充 Core XML inventory 后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/core-abstractions.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在补充 Core.Abstractions XML inventory 后重新验证
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `dotnet build GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`
|
||||
- 结果:通过
|
||||
- 备注:`0 Warning(s) / 0 Error(s)`
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`
|
||||
- 结果:通过
|
||||
- 备注:`0 Warning(s) / 0 Error(s)`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/index.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在重写 ECS landing 后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在重写 Arch ECS 专题页后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在补充抽象页 XML inventory 后重新验证
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在 Ecs 波次重写后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在重写 `Cqrs` family landing 后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在新增 `Cqrs.SourceGenerators` 专题页后验证通过
|
||||
- `python3` 轻量 XML inventory 扫描
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 确认 `GFramework.Cqrs` 的 `Internal/` 为 `14/14`、`GFramework.Cqrs.SourceGenerators/Cqrs/` 为 `3/3`、`GFramework.Cqrs.Abstractions/Cqrs/` 为 `20/20`
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -p:RestoreFallbackFolders=`
|
||||
- 结果:通过
|
||||
- 备注:保留既有 `NU1900` 与 `MA0051` warnings;无新增编译错误
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- 结果:失败
|
||||
- 备注:当前环境会命中失效的 Windows fallback package folder,并在多目标 inner build 阶段触发 `MSB4276` / `MSB4018`;失败原因已记录为环境阻塞,不属于本轮文档改动回归
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在 `Cqrs` 波次文档刷新后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/game-abstractions.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 在重写 `Game.Abstractions` 页面后验证通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/index.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 在补充 frontmatter 与 XML inventory 后重新验证
|
||||
- `python3` 轻量 XML inventory 扫描
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 确认 `GFramework.Game` 为 `56/56`、`GFramework.Game.Abstractions` 为 `80/80`、`GFramework.Game.SourceGenerators` 为 `2/2`
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 在 `Game` 波次文档刷新后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 在更新 `AGENTS.md` 的 WSL Git 优先级后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-23` 在推进 `DOCUMENTATION-FULL-COVERAGE-GOV-RP-007`、回写 `Game` family 巡检结论后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
@ -12,21 +12,57 @@
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-004`
|
||||
- 当前阶段:`Phase 3 - Cqrs Docs Refresh`
|
||||
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-016`
|
||||
- 当前阶段:`Phase 5 - Governance Maintenance`
|
||||
- 当前焦点:
|
||||
- 收口 `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` 的 landing / generator topic / API 入口
|
||||
- 延续 `README / landing / API reference / XML inventory` 的同一治理模板
|
||||
- 为下一波 `Game` family 审计保留统一的恢复模板与验证口径
|
||||
- 保持 `Game` family 的 persistence docs surface 与当前 `README`、源码、`PersistenceTests` 使用同一套 owner / adoption path 叙述
|
||||
- 将 `data.md`、`storage.md`、`serialization.md`、`setting.md` 视为 `Game` family 当前需要一起巡检的核心页面集,而不是分散的旧 API 手册页
|
||||
- 重点观察 `DataRepository` / `UnifiedSettingsDataRepository` / `SaveRepository<TSaveData>`、`FileStorage` / `ScopedStorage` 与 `SettingsModel<TRepository>` 的职责边界是否再次回漂
|
||||
- 在 `Game` runtime public API 或 README 再次变动前,优先做 targeted 巡检,不重复改写已稳定的 landing page
|
||||
|
||||
## 当前状态摘要
|
||||
|
||||
- 已归档的 `documentation-governance-and-refresh` 仅保留为历史证据,不再作为默认 `boot` 入口
|
||||
- `2026-04-23` 的 `Game` persistence docs wave 新增结论:
|
||||
- `docs/zh-CN/game/storage.md` 之前仍停留在旧版“通用存储 API 手册”写法,没有反映 `FileStorage` / `ScopedStorage` 与上层 repository 的分工,也没有强调当前同步 API 只是异步阻塞包装
|
||||
- `docs/zh-CN/game/data.md` 之前缺少 `DataRepository`、`UnifiedSettingsDataRepository` 与 `SaveRepository<TSaveData>` 三层分工,以及 `PersistenceTests` 已覆盖的备份 / 批量事件 / 存档迁移语义
|
||||
- `docs/zh-CN/game/serialization.md` 之前仍沿用“业务层手工 Serialize 再写回 storage”的旧示例,没有反映当前 `FileStorage` 已直接复用注入的 `ISerializer`
|
||||
- `docs/zh-CN/game/setting.md` 虽然已回到 `ISettingsModel` / `RegisterApplicator(...)` 口径,但缺少 frontmatter,且还没有和新的 `Game` persistence docs surface 使用同一套结构
|
||||
- `2026-04-23` 的 `Game` persistence docs wave 治理动作:
|
||||
- 重写 `docs/zh-CN/game/storage.md`,将其改为 `FileStorage` / `ScopedStorage` 的职责、路径语义、作用域复用与 repository 边界页
|
||||
- 重写 `docs/zh-CN/game/data.md`,补齐 `DataRepository`、`UnifiedSettingsDataRepository`、`SaveRepository<TSaveData>` 与 `DataRepositoryOptions` / `SaveConfiguration` 的当前契约
|
||||
- 重写 `docs/zh-CN/game/serialization.md`,收敛到 `JsonSerializer` 的配置生命周期、运行时类型序列化与和 storage / repository 的分工
|
||||
- 重写 `docs/zh-CN/game/setting.md`,使其与 `SettingsModel<TRepository>`、`SettingsSystem`、迁移缓存和统一设置仓库的当前实现保持一致
|
||||
- 本轮已消化的 PR #271 review follow-up:
|
||||
- 为 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 补齐 WSL worktree 下的显式 Linux Git 绑定,避免 `git.exe` 在当前会话触发 `Exec format error`
|
||||
- 同步更新 `.agents/skills/gframework-pr-review/SKILL.md`,改为与 `AGENTS.md` 一致的 Git 策略,并把命令示例统一到 `.agents/...` 路径
|
||||
- 为 `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` 补充 marker 类型放置与命名约定说明
|
||||
- 从 `docs/zh-CN/abstractions/ecs-arch-abstractions.md` 删除误放的 source-generator 内部模块提醒,并微调 `docs/zh-CN/ecs/index.md` 的边界说明语序
|
||||
- 为 `ai-plan/public/archive/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md` 的归档验证补写结果态
|
||||
- 将 RP-001 至 RP-007 的详细验证历史迁入 `ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-through-rp-007.md`
|
||||
- `2026-04-23` 再次执行 `$gframework-pr-review` 后,确认 PR `#271` 已关闭,latest reviewed commit `df91d3706ba9db71737e803ef2f40f4841ecbbf1` 仍显示 `2` 条 open thread,但两条都对应已在当前 HEAD 满足的 `ai-plan` 变更,属于 closed PR 上未自动收敛的陈旧线程信号
|
||||
- 本轮已确认的消费属性结论:
|
||||
- `GFramework.Ecs.Arch.Abstractions`:可打包直接消费模块,需要 README 和文档入口
|
||||
- `GFramework.Core.SourceGenerators.Abstractions`:`IsPackable=false`,按内部支撑模块处理
|
||||
- `GFramework.Godot.SourceGenerators.Abstractions`:`IsPackable=false`,按内部支撑模块处理
|
||||
- `GFramework.SourceGenerators.Common`:`IsPackable=false`,按内部支撑模块处理
|
||||
- 本轮已确认的 `Godot` family 恢复摘要:
|
||||
- `docs/zh-CN/godot/index.md`、`architecture.md`、`scene.md`、`ui.md`、`storage.md`、`setting.md`、`signal.md`、`extensions.md`、`logging.md` 与 `docs/zh-CN/tutorials/godot-integration.md` 是当前需要保留的核心页面集
|
||||
- `GFramework.Godot.SourceGenerators` 继续作为 `[GetNode]`、`[BindNodeSignal]`、`AutoLoads` 与 `InputActions` 的 owner;`GFramework.Godot.SourceGenerators.Abstractions` 仍按内部支撑模块处理
|
||||
- `Godot` Scene / UI 采用边界已经稳定:当前没有 `GodotSceneRouter` 或 `GodotUiRouter`;`GodotSceneFactory` 在 provider 缺失时会回退到 `SceneBehaviorFactory`,而 `GodotUiFactory` 仍要求 `IUiPageBehaviorProvider`
|
||||
- `2026-04-23` 的 validation-only 巡检新增结论:
|
||||
- 根 `README.md`、`docs/zh-CN/godot/index.md`、`docs/zh-CN/tutorials/godot-integration.md`、`docs/zh-CN/source-generators/index.md` 与 `docs/zh-CN/api-reference/index.md` 当前仍保持同一套 `Godot` owner / adoption path 叙述,没有发现新的入口漂移
|
||||
- `scan_module_evidence.py Godot` 显示 `docs/zh-CN/godot/storage.md` 与 `setting.md` 仍属于 `Godot` runtime docs surface,应并入 active topic 的最小恢复摘要,避免后续 `boot` 漏掉当前 landing page 的关键入口
|
||||
- `2026-04-23` 的子页巡检新增结论:
|
||||
- `docs/zh-CN/godot/storage.md` 之前仍停留在旧版 API 手册写法,缺少 frontmatter、`IStorage` / repository 边界和 `GodotYamlConfigLoader` 的分流说明
|
||||
- `docs/zh-CN/godot/setting.md` 之前仍把 `GodotAudioSettings` / `GodotGraphicsSettings` 描述成直接持有设置数据对象的旧构造方式,没有反映当前 `ISettingsModel` + `RegisterApplicator(...)` 接法
|
||||
- `2026-04-23` 的交叉链接巡检新增结论:
|
||||
- `GFramework.Godot/README.md` 仍停留在旧版简略描述,缺少当前包关系、子系统地图、最小接入路径与 `docs/zh-CN` 入口
|
||||
- `GFramework.Godot.SourceGenerators/README.md` 虽有示例,但没有覆盖 `AutoScene`、`AutoUiPage`、`AutoRegisterExportedCollections` 等当前生成器分组,也没有把运行时 / 生成器边界说清
|
||||
- `docs/zh-CN/api-reference/index.md` 的 `Godot` 行此前只把生成器入口指向泛化的 `source-generators/index.md`,不利于从 API 参考直接落到 `Godot` 专题页
|
||||
- `2026-04-23` 的后续入口巡检新增结论:
|
||||
- 根 `README.md` 的模块地图仍把 `GFramework.Godot.SourceGenerators` 写成“Godot 场景专用源码生成器”,范围过窄,不符合当前 `project.godot` 元数据、节点注入、信号绑定、Scene / UI 包装与导出集合注册的真实职责
|
||||
- `docs/zh-CN/source-generators/index.md` 的选包说明此前只提到 `AutoLoad / Input Action` 与节点 / 信号样板,没有把 Scene / UI 包装与导出集合注册辅助纳入同一入口
|
||||
- 本轮已完成的治理动作:
|
||||
- 新建 `GFramework.Ecs.Arch.Abstractions/README.md`
|
||||
- 在根 `README.md` 中补齐 `GFramework.Ecs.Arch.Abstractions` 入口,并声明内部支撑模块 owner
|
||||
@ -47,6 +83,22 @@
|
||||
- 新建 `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`,为 `Cqrs.SourceGenerators` 补齐站内专题入口
|
||||
- 更新 `docs/zh-CN/source-generators/index.md`、`docs/zh-CN/api-reference/index.md` 与 VitePress sidebar,使 `Cqrs` family 的 generator 入口可导航
|
||||
- 为 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs` 与 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中缺失的内部类型补齐 XML 注释,使本轮轻量 inventory 达到声明级闭环
|
||||
- 为 `GFramework.Game/README.md`、`GFramework.Game.Abstractions/README.md`、`GFramework.Game.SourceGenerators/README.md` 补齐 `Game` family 的类型族级 XML inventory
|
||||
- 为 `docs/zh-CN/game/index.md` 补齐 frontmatter,并增加 `Game` / `Game.Abstractions` / `Game.SourceGenerators` 的 XML 覆盖基线入口
|
||||
- 将 `docs/zh-CN/abstractions/game-abstractions.md` 从失真的旧接口摘录页重写为契约边界 / 包关系 / 最小接入路径页面
|
||||
- 基于顶层目录轻量盘点确认:`GFramework.Game` 为 `56/56`、`GFramework.Game.Abstractions` 为 `80/80`、`GFramework.Game.SourceGenerators` 为 `2/2`,当前公开 / 内部类型声明都已带 XML 注释
|
||||
- 更新 `AGENTS.md` 的 WSL Git 策略,将显式 `--git-dir` / `--work-tree` 绑定提升为高于 `git.exe` 的默认优先级
|
||||
- 记录当前环境偏差:本会话 `git.exe` 可解析但执行会触发 `Exec format error`,而 plain Linux `git` 会命中 worktree 路径翻译错误,需要显式仓库绑定
|
||||
- 完成 `Game` family 巡检,确认 `docs/zh-CN/game/config-system.md`、`scene.md`、`ui.md` 与 `docs/zh-CN/source-generators/index.md` 的核心采用说明、包关系与交叉引用仍与当前源码 / README 一致,没有发现需要立刻修正的回漂
|
||||
- 将 `Godot` family 的最小恢复摘要迁回 active topic,保留核心页面集、生成器 owner、Scene / UI 真实边界与归档指针,避免长期治理默认恢复路径继续依赖 archive 明细
|
||||
- 重写 `GFramework.Godot/README.md`,补齐包定位、相邻包关系、子系统地图、最小接入路径与站内文档入口,并明确它不拥有生成器职责
|
||||
- 重写 `GFramework.Godot.SourceGenerators/README.md`,按 `project.godot` 元数据、节点注入 / 信号绑定、行为包装与批量注册四组能力重建入口
|
||||
- 更新 `docs/zh-CN/api-reference/index.md` 的 `Godot` 模块映射,使 API 参考能直接落到 `Godot` 专用生成器专题页,而不是仅回到总览页
|
||||
- 修正根 `README.md` 中 `GFramework.Godot.SourceGenerators` 的模块描述,使其与当前生成器职责边界一致
|
||||
- 扩充 `docs/zh-CN/source-generators/index.md` 的 Godot 选包说明,把 Scene / UI 包装与导出集合注册辅助纳入入口摘要
|
||||
- 重写 `docs/zh-CN/godot/storage.md`,补齐 frontmatter、`GodotFileStorage` 的路径语义、repository 分工与 `GodotYamlConfigLoader` 分流边界
|
||||
- 重写 `docs/zh-CN/godot/setting.md`,改回当前 `ISettingsModel` / `RegisterApplicator(...)` 口径,并补上 `LocalizationMap` fallback 与 `CoreGrid` 注册示例
|
||||
- `2026-04-23` 再次通过 `$gframework-boot` 恢复当前 worktree 后,按 `Godot` docs surface 执行 validation-only 巡检,确认 `GFramework.Godot/README.md`、`docs/zh-CN/godot/index.md`、`storage.md`、`setting.md`、`docs/zh-CN/source-generators/index.md`、`docs/zh-CN/api-reference/index.md` 与 `docs/zh-CN/tutorials/godot-integration.md` 仍保持一致的 owner / adoption path 叙述,没有出现自 `RP-014` 之后的新漂移
|
||||
|
||||
## Inventory(第一版)
|
||||
|
||||
@ -54,8 +106,8 @@
|
||||
| --- | --- | --- | --- |
|
||||
| `Core` / `Core.Abstractions` | `README / landing / 类型族级 XML inventory 已收口,成员级审计待补齐` | 根 README、模块 README、`docs/zh-CN/core/**`、`docs/zh-CN/abstractions/core-abstractions.md` 已对齐当前目录与类型族基线 | 进入巡检;如有新 API 变更,再追加成员级 XML 审计 |
|
||||
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | `README / landing / generator topic / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Cqrs/README.md`、`GFramework.Cqrs.Abstractions/README.md`、`GFramework.Cqrs.SourceGenerators/README.md`、`docs/zh-CN/core/cqrs.md`、`docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`、`docs/zh-CN/api-reference/index.md` 已对齐当前源码与测试 | 转入巡检;下一波切到 `Game` family 的 XML / 教程链路审计 |
|
||||
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | `已验证` | 根 README、模块 README、`docs/zh-CN/game/**` 和 abstractions 页已存在 | 后续波次补 XML / 教程链路审计 |
|
||||
| `Godot` / `Godot.SourceGenerators` | `已验证` | 上一轮归档 topic 已完成核心 landing / topic / tutorial 校验 | 进入巡检周期,重点看回漂 |
|
||||
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | `README / landing / abstractions / persistence topic pages / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Game/README.md`、`GFramework.Game.Abstractions/README.md`、`GFramework.Game.SourceGenerators/README.md`、`docs/zh-CN/game/index.md`、`docs/zh-CN/abstractions/game-abstractions.md`、`docs/zh-CN/game/data.md`、`storage.md`、`serialization.md`、`setting.md` 已对齐当前源码、README 与 `PersistenceTests` | 转入巡检;优先观察后续分支是否再次把 `Game` persistence docs 写回旧 API 手册口径 |
|
||||
| `Godot` / `Godot.SourceGenerators` | `README / 生成器 README / landing / topic / tutorial / API reference 入口已重新对齐,成员级 XML 审计不在本轮范围` | `GFramework.Godot/README.md`、`GFramework.Godot.SourceGenerators/README.md`、`docs/zh-CN/godot/index.md`、`architecture.md`、`scene.md`、`ui.md`、`storage.md`、`setting.md`、`signal.md`、`extensions.md`、`logging.md`、`docs/zh-CN/tutorials/godot-integration.md` | 进入巡检周期,优先观察后续分支是否再次把 README / API 入口写回过时边界 |
|
||||
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | `README / landing / abstractions / 类型族级 XML inventory 已收口,成员级审计待补齐` | `GFramework.Ecs.Arch/README.md`、`GFramework.Ecs.Arch.Abstractions/README.md`、`docs/zh-CN/ecs/**`、`docs/zh-CN/abstractions/ecs-arch-abstractions.md` 已对齐当前源码与测试 | 转入巡检;后续仅在运行时公共 API 变动时补成员级 XML 细审 |
|
||||
| `SourceGenerators.Common` 与 `*.SourceGenerators.Abstractions` | `已判定为内部支撑` | `*.csproj` 明确 `IsPackable=false` | 由所属模块 README 与生成器栏目说明 owner,不建独立采用页 |
|
||||
|
||||
@ -76,71 +128,68 @@
|
||||
|
||||
- 当前 `Core` / `Core.Abstractions` 只完成了类型族级 XML 基线,不等于成员级契约全审计
|
||||
- 缓解措施:后续只在共享抽象或高风险生命周期接口发生改动时补成员级细审,不在本轮扩张范围
|
||||
- 其他模块族尚未全部建立同粒度的 XML inventory
|
||||
- 缓解措施:按 `Ecs`、`Cqrs`、`Game` 的波次顺序继续推广同一模板
|
||||
- `Godot` family 的详细治理历史仍保留在 archive,active topic 只回填了最小恢复摘要
|
||||
- 缓解措施:active topic 记录核心页面集、owner、运行时边界与 archive 指针;只有在需要阶段级历史时再读取归档材料
|
||||
- 新功能分支若修改 README / docs / 公共 API 却不挂文档 topic,仍可能回漂
|
||||
- 缓解措施:将本 topic 作为长期 active topic 保留,并在后续巡检中记录回漂来源
|
||||
- VitePress 页面不能直接链接到 `docs/` 目录之外的模块 `README.md`
|
||||
- 缓解措施:站内页面用模块路径文本或站内 API 入口表达,仓库级 README 仍保留仓库文件链接
|
||||
- `GFramework.Cqrs` 在当前 WSL / dotnet 环境下,本地 build 仍会读取失效的 fallback package folder 配置,导致无法完成该项目的标准编译验证
|
||||
- 缓解措施:本轮先以 `GFramework.Cqrs.SourceGenerators` 编译通过和 docs site build 通过作为有效验证,并在后续环境治理或构建脚本清理时单独处理 `RestoreFallbackFolders` / 资产文件问题
|
||||
- 当前 WSL 会话中 `git.exe` 虽然可解析,但不能执行
|
||||
- 缓解措施:把显式 `--git-dir` / `--work-tree` 绑定上升为仓库默认回退策略,并仅把 `git.exe` 保留为可执行时的次级 fallback
|
||||
|
||||
## 验证说明
|
||||
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/index.md`
|
||||
- 结果:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`
|
||||
- 结果:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`
|
||||
- 结果:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在补充 Core XML inventory 后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/core-abstractions.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在补充 Core.Abstractions XML inventory 后重新验证
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `dotnet build GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`
|
||||
- 结果:通过
|
||||
- 备注:`0 Warning(s) / 0 Error(s)`
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj -c Release -p:RestoreFallbackFolders=`
|
||||
- 结果:通过
|
||||
- 备注:`0 Warning(s) / 0 Error(s)`
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/index.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在重写 ECS landing 后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在重写 Arch ECS 专题页后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在补充抽象页 XML inventory 后重新验证
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在 Ecs 波次重写后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在重写 `Cqrs` family landing 后重新验证
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在新增 `Cqrs.SourceGenerators` 专题页后验证通过
|
||||
- `python3` 轻量 XML inventory 扫描
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 确认 `GFramework.Cqrs` 的 `Internal/` 为 `14/14`、`GFramework.Cqrs.SourceGenerators/Cqrs/` 为 `3/3`、`GFramework.Cqrs.Abstractions/Cqrs/` 为 `20/20`
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release -p:RestoreFallbackFolders=`
|
||||
- 结果:通过
|
||||
- 备注:保留既有 `NU1900` 与 `MA0051` warnings;无新增编译错误
|
||||
- `DOTNET_CLI_HOME=/tmp/dotnet-home dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- 结果:失败
|
||||
- 备注:当前环境会命中失效的 Windows fallback package folder,并在多目标 inner build 阶段触发 `MSB4276` / `MSB4018`;失败原因已记录为环境阻塞,不属于本轮文档改动回归
|
||||
- `cd docs && bun run build`
|
||||
- 结果:通过
|
||||
- 备注:`2026-04-22` 在 `Cqrs` 波次文档刷新后重新构建通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
- 详细验证历史已归档到 `ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-through-rp-007.md`
|
||||
- 最新 PR review 结论:
|
||||
- `2026-04-23` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
|
||||
- 结果:通过;PR `#271` 已关闭,latest reviewed commit 为 `df91d3706ba9db71737e803ef2f40f4841ecbbf1`,当前 `2` 条 open thread 都是已被本地文件满足的陈旧信号,不再构成本轮阻塞
|
||||
- 最新构建结论:
|
||||
- `2026-04-23` `cd docs && bun run build`
|
||||
- 结果:通过;在重写 `docs/zh-CN/game/data.md`、`storage.md`、`serialization.md`、`setting.md` 后再次验证通过,仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
- 最新 `Game` persistence docs wave 结论:
|
||||
- `2026-04-23` 基于 `GFramework.Game` 源码、`GFramework.Game/README.md`、`JsonSerializerTests`、`SettingsModelTests` 与 `PersistenceTests`
|
||||
- 结果:通过;`docs/zh-CN/game/data.md`、`storage.md`、`serialization.md`、`setting.md` 当前已回到同一套 `Game` runtime 持久化采用路径,不再沿用旧版 API 手册叙述
|
||||
- 最新稳定性巡检结论:
|
||||
- `2026-04-23` 重新执行 `Godot` docs surface 巡检
|
||||
- 结果:通过;根入口链路保持稳定,并额外发现 `docs/zh-CN/godot/storage.md`、`setting.md` 两页存在旧版叙述残留,当前已按源码口径完成最小修复
|
||||
- 最新 validation-only 巡检结论:
|
||||
- `2026-04-23` 通过 `$gframework-boot` 恢复后重新执行 `Godot` docs surface 巡检
|
||||
- 结果:通过;`README / landing / topic / tutorial / API reference` 当前仍保持同一套 `Godot` owner / adoption path 叙述,本轮无需新增文档补丁
|
||||
- 最新恢复治理结论:
|
||||
- `2026-04-23` 重新读取 `ai-plan/public/archive/documentation-governance-and-refresh/**`
|
||||
- 结果:通过;确认 `Godot` family 适合把最小恢复摘要迁回 active topic,但不需要把整段归档历史重新放回默认 `boot` 路径
|
||||
- 已完成的针对性校验:
|
||||
- `2026-04-23` `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot`:通过
|
||||
- `2026-04-23` `python3 -B -c "from pathlib import Path; compile(Path('.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py').read_text(encoding='utf-8'), '.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py', 'exec')"`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/index.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Godot/README.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh GFramework.Godot/README.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Godot.SourceGenerators/README.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh GFramework.Godot.SourceGenerators/README.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh README.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh README.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/index.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`:通过
|
||||
- `2026-04-23` `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot`:通过(boot 后复核)
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`:通过(boot 后复核)
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`:通过(boot 后复核)
|
||||
- `2026-04-23` `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Game`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/data.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/storage.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/serialization.md`:通过
|
||||
- `2026-04-23` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/setting.md`:通过
|
||||
- `2026-04-23` `cd docs && bun run build`:通过(本轮 `Game` persistence docs wave 复核;仅保留既有 VitePress 大 chunk warning)
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 切换到 `Game` / `Game.Abstractions` / `Game.SourceGenerators` 波次,按 `Cqrs` 模板核对 README / landing / tutorials / API reference / XML 链路
|
||||
2. 评估 `Game` family 当前是否已经具备类型族级 XML inventory,还是仍停留在“README / 页面存在但不可审计”
|
||||
3. 在后续环境治理任务中单独处理 `GFramework.Cqrs` 本地 build 的 fallback package folder 阻塞,避免影响后续代码类验证
|
||||
1. 若后续分支继续调整 `GFramework.Game` 的 persistence runtime、README 或公共 API,优先复核 `docs/zh-CN/game/data.md`、`storage.md`、`serialization.md`、`setting.md` 与 landing page 是否仍保持同一套职责边界
|
||||
2. 当 `Godot` / `Game` family 再次出现交叉入口漂移时,沿用当前 README -> landing -> topic page -> API reference 的最小修复顺序
|
||||
3. 仅在需要阶段级细节时再读取 `documentation-governance-and-refresh` archive,而不是把 archive 重新当作默认 `boot` 入口
|
||||
|
||||
@ -141,3 +141,502 @@
|
||||
|
||||
1. 切换到 `Game` family 波次,按 `Core` / `Ecs` / `Cqrs` 已验证模板继续补 XML inventory 与教程链路
|
||||
2. 把 `GFramework.Cqrs` 的本地构建阻塞留给后续环境治理或构建脚本清理,不在本 topic 内扩张为环境修复任务
|
||||
|
||||
### 当前恢复点:RP-005
|
||||
|
||||
- 完成 `Game` 波次的模块族入口刷新:
|
||||
- 更新 `GFramework.Game/README.md`
|
||||
- 更新 `GFramework.Game.Abstractions/README.md`
|
||||
- 更新 `GFramework.Game.SourceGenerators/README.md`
|
||||
- 更新 `docs/zh-CN/game/index.md`
|
||||
- 重写 `docs/zh-CN/abstractions/game-abstractions.md`
|
||||
- 将 `Game` family 从“README / 页面存在但缺少可审计 XML 入口,且 abstractions 页失真”推进到“runtime / abstractions / source generator 都有声明级 XML inventory 与真实采用边界”
|
||||
- 基于轻量扫描确认:
|
||||
- `GFramework.Game` 当前类型声明级 XML 覆盖为 `56/56`
|
||||
- `GFramework.Game.Abstractions` 当前类型声明级 XML 覆盖为 `80/80`
|
||||
- `GFramework.Game.SourceGenerators` 当前类型声明级 XML 覆盖为 `2/2`
|
||||
|
||||
### 当前决策(RP-005)
|
||||
|
||||
- `docs/zh-CN/abstractions/game-abstractions.md` 不再维护虚构接口摘录,而是与源码中的 `Config` / `Data` / `Setting` / `Scene` / `UI` / `Routing` 契约分组保持一致
|
||||
- `Game.SourceGenerators` 继续以 `README + docs/zh-CN/game/config-system.md + docs/zh-CN/source-generators/index.md` 组成入口,不额外新增只为凑数量的专题页
|
||||
- `docs/zh-CN/game/index.md` 补 frontmatter,并承担 `Game` family 的 XML 基线入口;更细的类型族说明继续留在模块 README 与 abstractions 页
|
||||
|
||||
### 当前验证(RP-005)
|
||||
|
||||
- 文档校验:
|
||||
- `validate-all.sh docs/zh-CN/abstractions/game-abstractions.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/game/index.md`:通过
|
||||
- 轻量 XML inventory:
|
||||
- `GFramework.Game`:`56/56`
|
||||
- `GFramework.Game.Abstractions`:`80/80`
|
||||
- `GFramework.Game.SourceGenerators`:`2/2`
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 进入 `Game` family 巡检,优先检查 `config-system.md`、`scene.md`、`ui.md` 与 `source-generators/index.md` 的交叉引用是否回漂
|
||||
2. 评估是否需要把 `Godot` family 的关键 XML inventory 摘要迁回 active topic,减少对 archive 的依赖
|
||||
|
||||
### 当前恢复点:RP-006
|
||||
|
||||
- 更新 `AGENTS.md` 的 WSL Git 规则:
|
||||
- 将显式 `git --git-dir=<...> --work-tree=<...>` 绑定提升为高于 `git.exe` 的默认优先级
|
||||
- 明确 plain Linux `git` 命中 worktree 路径翻译错误时,应先切到显式绑定而不是直接改用 `git.exe`
|
||||
- 明确 `git.exe` 只有在当前会话可执行时才作为次级 fallback
|
||||
- 记录本次恢复任务的环境偏差:
|
||||
- `git.exe` 在当前 WSL 会话中可解析,但执行会触发 `Exec format error`
|
||||
- plain `git` 会把 worktree 元数据路径翻译错并报“not a git repository”
|
||||
- 显式 `--git-dir` / `--work-tree` 绑定是本次已验证可用的 Git 操作方式
|
||||
|
||||
### 当前决策(RP-006)
|
||||
|
||||
- 把 Git 回退顺序写进 `AGENTS.md`,而不是只留在一次性的聊天上下文里
|
||||
- 不额外扩张 `gframework-boot` skill,因为它本身不内嵌 Git 选择逻辑,继续由 `AGENTS.md` 作为唯一准则
|
||||
- 继续把 `git.exe` 保留为 fallback,而不是完全删除,避免在可执行的 WSL 会话里丢掉可用路径
|
||||
|
||||
### 当前验证(RP-006)
|
||||
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 继续 `Game` family 巡检,优先检查 `config-system.md`、`scene.md`、`ui.md` 与 `source-generators/index.md` 的交叉引用是否回漂
|
||||
2. 评估是否需要把 `Godot` family 的关键 XML inventory 摘要迁回 active topic,减少对 archive 的依赖
|
||||
|
||||
### 当前恢复点:RP-007
|
||||
|
||||
- 完成 `Game` family 巡检:
|
||||
- 复核 `docs/zh-CN/game/config-system.md`
|
||||
- 复核 `docs/zh-CN/game/scene.md`
|
||||
- 复核 `docs/zh-CN/game/ui.md`
|
||||
- 复核 `docs/zh-CN/source-generators/index.md`
|
||||
- 对照 `GFramework.Game`、`GFramework.Game.Abstractions`、`GFramework.Game.SourceGenerators` README 与相关源码 / 测试后,未发现需要立刻修正的采用语义回漂
|
||||
- 重点确认的真实语义包括:
|
||||
- `GameConfigBootstrap` / `RegisterAllGeneratedConfigTables(...)` / `GFrameworkConfigSchemaDirectory` 的配置入口仍与文档示例一致
|
||||
- `SceneRouterBase` 仍通过 `SemaphoreSlim` 串行化切换,并拒绝重复 `sceneKey` 入栈
|
||||
- `UiRouterBase` 仍将 `Page` 层与 `Overlay` / `Modal` / `Toast` / `Topmost` 分为两套入口,且 `Show(..., UiLayer.Page)` 会直接拒绝
|
||||
|
||||
### 当前决策(RP-007)
|
||||
|
||||
- 本轮不为“巡检通过”硬造文档改动,先把结论写回 active topic,保持恢复点准确
|
||||
- `Game` family 暂时转入稳定巡检,不在没有源码变化的情况下重复改写 landing page
|
||||
- 默认下一步切到 `Godot` family 摘要是否回迁,减少长期治理对 archive topic 的依赖
|
||||
|
||||
### 当前验证(RP-007)
|
||||
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 评估是否需要把 `Godot` family 的关键 XML inventory 摘要迁回 active topic
|
||||
2. 若不需要迁回,则继续抽查 README / landing page / API reference 之间的 cross-link 是否出现新的漂移
|
||||
|
||||
### 当前恢复点:RP-008
|
||||
|
||||
- 使用 `$gframework-pr-review` 抓取当前分支 PR `#271` 后,确认 latest head review threads 仍有 `4` 条 open:
|
||||
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md` 的 marker 类型约定说明缺口
|
||||
- `docs/zh-CN/ecs/index.md` 的边界说明语序问题
|
||||
- `docs/zh-CN/abstractions/ecs-arch-abstractions.md` 误放的 source-generator 内部模块提醒
|
||||
- `ai-plan/public/documentation-full-coverage-governance/todos/documentation-full-coverage-governance-tracking.md` 的验证历史过长,以及
|
||||
`ai-plan/public/archive/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md` 缺少显式结果态
|
||||
- 在当前 WSL 会话里,`gframework-pr-review` 脚本先命中了 `git.exe` 的 `Exec format error`
|
||||
- 已将 `.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py` 改为优先使用 Linux `git` 的显式
|
||||
`--git-dir` / `--work-tree` 绑定,并仅在无法建立该绑定时回退到旧的可执行解析逻辑
|
||||
- 已同步更新 `.agents/skills/gframework-pr-review/SKILL.md`,使其 Git 策略与命令示例都与当前仓库状态一致
|
||||
- 已把 `DOCUMENTATION-FULL-COVERAGE-GOV-RP-001` 到 `RP-007` 的详细验证历史迁入
|
||||
`ai-plan/public/documentation-full-coverage-governance/archive/todos/documentation-full-coverage-governance-validation-history-through-rp-007.md`
|
||||
|
||||
### 当前决策(RP-008)
|
||||
|
||||
- 继续把 latest-head unresolved threads 作为主信号,只修仍在本地成立的评论,不为已失效的历史 summary 做无意义回写
|
||||
- active tracking 只保留最新验证摘要与恢复点;详细验证历史留在 topic 自己的 archive,而不是继续堆在默认 boot 路径
|
||||
- `gframework-pr-review` 的脚本行为、技能文案与 `AGENTS.md` 必须保持同一套 WSL Git 策略,避免再次出现“文档说法正确但工具实现仍跑偏”的情况
|
||||
|
||||
### 当前验证(RP-008)
|
||||
|
||||
- PR review 抓取:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`:通过
|
||||
- 脚本语法校验:
|
||||
- `python3 -B -c "from pathlib import Path; compile(Path('.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py').read_text(encoding='utf-8'), '.agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py', 'exec')"`:通过
|
||||
- 文档校验:
|
||||
- `validate-all.sh docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/ecs/index.md`:通过
|
||||
- `validate-all.sh docs/zh-CN/abstractions/ecs-arch-abstractions.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 提交本轮 PR review follow-up
|
||||
2. 推送当前分支后重新执行 `$gframework-pr-review`,观察 PR #271 的 open threads 是否收敛
|
||||
|
||||
### 当前恢复点:RP-009
|
||||
|
||||
- 按 `boot` 恢复 `documentation-full-coverage-governance` 主题
|
||||
- 重新读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md` 与当前 topic 的 active todo / trace 后,确认当前 worktree `docs/sdk-update-documentation` 仍映射到本 topic
|
||||
- 当前 worktree Git 状态干净,且不存在 `ai-plan/private/` 的 worktree 私有恢复材料
|
||||
- 重新执行 `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
|
||||
- 抓取结果显示 PR `#271` 已关闭,latest reviewed commit 仍为 `df91d3706ba9db71737e803ef2f40f4841ecbbf1`
|
||||
- 当前 latest commit 仍显示 `2` 条 open thread,但两条都落在 `ai-plan` 文件上,且本地文件已经满足评论要求:
|
||||
- `ai-plan/public/archive/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md` 已包含显式 `结果:通过`
|
||||
- `ai-plan/public/documentation-full-coverage-governance/todos/documentation-full-coverage-governance-tracking.md` 已将 RP-001 至 RP-007 的详细验证明细迁入 archive
|
||||
- 因此本轮将 PR #271 follow-up 视为已完成,后续不再为 closed PR 上未自动收敛的陈旧 thread 状态追加仓库改动
|
||||
|
||||
### 当前决策(RP-009)
|
||||
|
||||
- `closed PR + stale open thread` 不再作为需要继续修改仓库内容的信号;除非后续 review 抓取显示新的 latest-head finding
|
||||
- `documentation-full-coverage-governance` 的默认下一步切回治理 backlog,优先判断是否把 `Godot` family 的关键 XML inventory 摘要迁回 active topic
|
||||
- 本轮 `boot` 不引入 subagent;关键恢复信号都能通过本地读取和单次 PR review 抓取直接确认
|
||||
|
||||
### 当前验证(RP-009)
|
||||
|
||||
- PR review 抓取:
|
||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`:通过;PR `#271` 已关闭,latest reviewed commit 为 `df91d3706ba9db71737e803ef2f40f4841ecbbf1`,当前 `2` 条 open thread 都是已被本地文件满足的陈旧信号
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 评估是否需要把 `Godot` family 的关键 XML inventory 摘要迁回 active topic
|
||||
2. 若不迁回,则在 active todo / trace 保留足够的 archive 指针,并继续抽查 README / landing page / API reference 的 cross-link 是否出现新的漂移
|
||||
|
||||
### 当前恢复点:RP-010
|
||||
|
||||
- 按 `boot` 恢复当前 topic 后,重新读取:
|
||||
- `AGENTS.md`
|
||||
- `.ai/environment/tools.ai.yaml`
|
||||
- `ai-plan/public/README.md`
|
||||
- `ai-plan/public/documentation-full-coverage-governance/todos/documentation-full-coverage-governance-tracking.md`
|
||||
- `ai-plan/public/documentation-full-coverage-governance/traces/documentation-full-coverage-governance-trace.md`
|
||||
- 确认当前任务状态属于 `resume`:
|
||||
- 当前分支仍为 `docs/sdk-update-documentation`
|
||||
- `ai-plan/public/README.md` 继续把本 worktree 映射到 `documentation-full-coverage-governance`
|
||||
- 当前 worktree 没有 `ai-plan/private/` 私有恢复材料
|
||||
- 为判断 `Godot` family 是否需要回填恢复摘要,补读归档主题:
|
||||
- `ai-plan/public/archive/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md`
|
||||
- `ai-plan/public/archive/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md`
|
||||
- `ai-plan/public/archive/documentation-governance-and-refresh/archive/todos/documentation-governance-and-refresh-history-through-2026-04-22.md`
|
||||
- 归档材料表明,`Godot` family 的可恢复关键信号已经稳定,且足以压缩成 active topic 里的最小摘要:
|
||||
- 核心页面集为 `docs/zh-CN/godot/index.md`、`architecture.md`、`scene.md`、`ui.md`、`signal.md`、`extensions.md`、`logging.md` 与 `docs/zh-CN/tutorials/godot-integration.md`
|
||||
- `GFramework.Godot.SourceGenerators` 继续作为 `[GetNode]`、`[BindNodeSignal]`、`AutoLoads`、`InputActions` 的 owner
|
||||
- `GFramework.Godot.SourceGenerators.Abstractions` 继续按 `IsPackable=false` 的内部支撑模块处理
|
||||
- `GodotSceneFactory` 在 provider 缺失时回退到 `SceneBehaviorFactory`,而 `GodotUiFactory` 仍要求 `IUiPageBehaviorProvider`
|
||||
- 因此本轮决定:
|
||||
- 不把整段 `documentation-governance-and-refresh` 历史重新迁回 active 路径
|
||||
- 只把足够让未来 `boot` 快速恢复的 `Godot` family 摘要写回 active todo
|
||||
- 继续把阶段级细节留在 archive,保持默认恢复入口轻量
|
||||
|
||||
### 当前决策(RP-010)
|
||||
|
||||
- `Godot` family 的“最小恢复摘要”应当留在 active topic,因为它已经属于长期治理 backlog 的默认上下文,而不仅仅是已完成项目的历史注脚
|
||||
- active topic 只保留对后续判断有用的事实:
|
||||
- 页面范围
|
||||
- generator owner
|
||||
- Scene / UI 真实运行时边界
|
||||
- archive 指针
|
||||
- `documentation-governance-and-refresh` archive 继续作为阶段级历史证据,不重新回到 `boot` 默认扫描路径
|
||||
- 下一步从“是否回填摘要”切换回“继续巡检 cross-link 漂移”,避免治理入口停留在已经完成的元问题上
|
||||
|
||||
### 当前验证(RP-010)
|
||||
|
||||
- 归档恢复检查:
|
||||
- `sed -n '1,260p' ai-plan/public/archive/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md`:通过
|
||||
- `sed -n '1,260p' ai-plan/public/archive/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md`:通过
|
||||
- `sed -n '1,240p' ai-plan/public/archive/documentation-governance-and-refresh/archive/todos/documentation-governance-and-refresh-history-through-2026-04-22.md`:通过
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 抽查 `Godot` 与 `Game` 相关 README / landing page / API reference 的 cross-link 是否出现新的漂移
|
||||
2. 当后续分支修改相关 README / docs / 公共 API 时,回到对应 module family 追加 targeted 巡检与验证
|
||||
|
||||
### 当前恢复点:RP-011
|
||||
|
||||
- 继续按 `boot` 恢复后的默认下一步执行 `Godot` / `Game` cross-link 巡检,并额外补读:
|
||||
- `GFramework.Godot/README.md`
|
||||
- `GFramework.Godot.SourceGenerators/README.md`
|
||||
- `docs/zh-CN/api-reference/index.md`
|
||||
- `docs/zh-CN/godot/index.md`
|
||||
- `docs/zh-CN/source-generators/index.md`
|
||||
- 结合 `GFramework.Godot.csproj`、`GFramework.Godot.SourceGenerators.csproj`、相关测试与 `scan_module_evidence.py` 输出,确认新的漂移点集中在入口 README:
|
||||
- `GFramework.Godot/README.md` 仍是旧版简略说明,没有记录当前包关系、子系统地图、最小接入路径与 `docs/zh-CN` 入口
|
||||
- `GFramework.Godot.SourceGenerators/README.md` 没有覆盖 `AutoScene`、`AutoUiPage`、`AutoRegisterExportedCollections` 这些当前已发布的生成器分组
|
||||
- `docs/zh-CN/api-reference/index.md` 的 `Godot` 映射仍只把生成器入口落到泛化总览页,恢复效率偏低
|
||||
- 因此本轮执行最小修复集:
|
||||
- 重写 `GFramework.Godot/README.md`
|
||||
- 重写 `GFramework.Godot.SourceGenerators/README.md`
|
||||
- 更新 `docs/zh-CN/api-reference/index.md` 的 `Godot` 行
|
||||
|
||||
### 当前决策(RP-011)
|
||||
|
||||
- 这轮不改 `docs/zh-CN/godot/**` landing / topic 页面,因为站内页面本身没有发现新的事实漂移,问题集中在仓库 README 与 API 入口的回退
|
||||
- `GFramework.Godot` README 必须和 `Game` / `Godot` 真实边界一致,明确它不是生成器 owner,也不引入虚构的 router 类型
|
||||
- `GFramework.Godot.SourceGenerators` README 采用“元数据 / 节点注入与信号绑定 / 行为包装 / 批量注册”四段式入口,避免读者只看到旧的三项能力
|
||||
- API 参考页对 `Godot` 生成器入口直接给出专题页链接,而不是仅要求读者再从总览页二次分流
|
||||
|
||||
### 当前验证(RP-011)
|
||||
|
||||
- 模块扫描:
|
||||
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot`:通过
|
||||
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot.SourceGenerators`:通过
|
||||
- 文档校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Godot/README.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh GFramework.Godot/README.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Godot.SourceGenerators/README.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh GFramework.Godot.SourceGenerators/README.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 继续抽查根 `README.md`、`docs/zh-CN/source-generators/index.md` 与 `docs/zh-CN/tutorials/godot-integration.md` 是否仍把 `Godot` owner 写回旧边界
|
||||
2. 当后续分支继续修改 `Game` / `Godot` family 入口时,沿用当前 README -> landing -> API reference 的最小修复顺序
|
||||
|
||||
### 当前恢复点:RP-012
|
||||
|
||||
- 继续按 `boot` 恢复后的默认下一步执行 `Game` / `Godot` 入口巡检,并重新读取:
|
||||
- `README.md`
|
||||
- `docs/zh-CN/source-generators/index.md`
|
||||
- `docs/zh-CN/tutorials/godot-integration.md`
|
||||
- `docs/zh-CN/api-reference/index.md`
|
||||
- `GFramework.Godot/README.md`
|
||||
- `GFramework.Godot.SourceGenerators/README.md`
|
||||
- 巡检结果显示主体内容仍然稳定,但根入口摘要存在一处残留漂移:
|
||||
- 根 `README.md` 仍把 `GFramework.Godot.SourceGenerators` 写成“Godot 场景专用源码生成器”,与当前包实际覆盖的 `project.godot` 元数据、节点注入、信号绑定、Scene / UI 包装和导出集合注册职责不符
|
||||
- `docs/zh-CN/source-generators/index.md` 的选包描述同步缺少 Scene / UI 包装与导出集合注册辅助这组能力
|
||||
- 因此本轮执行最小修复集:
|
||||
- 更新根 `README.md` 的 `GFramework.Godot.SourceGenerators` 模块描述
|
||||
- 更新 `docs/zh-CN/source-generators/index.md` 的 Godot 选包摘要
|
||||
|
||||
### 当前决策(RP-012)
|
||||
|
||||
- 继续维持“只修新发现的入口漂移,不重写稳定页面”的治理节奏;这轮不改 `docs/zh-CN/tutorials/godot-integration.md`,因为教程与 README / 生成器专题页仍使用同一套职责边界
|
||||
- 根 `README.md` 作为仓库一级入口,必须与模块 README 保持同一粒度的职责摘要;如果根入口比模块 README 更旧,后续 `boot` 和人工恢复都会被误导
|
||||
- `source-generators/index.md` 的选包段落需要覆盖当前真实能力分组,但不重复展开各专题页细节,避免重新长成第二份 README
|
||||
|
||||
### 当前验证(RP-012)
|
||||
|
||||
- 文档校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh README.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-code-blocks.sh README.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators/index.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 继续抽查 `docs/zh-CN/tutorials/godot-integration.md`、`docs/zh-CN/godot/index.md` 与根 `README.md` 的职责摘要是否继续保持同一口径
|
||||
2. 当后续分支继续修改 `Game` / `Godot` family 入口时,沿用当前 README -> landing -> API reference 的最小修复顺序
|
||||
|
||||
### 当前恢复点:RP-013
|
||||
|
||||
- 使用 `$gframework-boot` 恢复当前 worktree 后,按 `documentation-full-coverage-governance` 的默认下一步执行一次
|
||||
validation-only 巡检,并补读:
|
||||
- `README.md`
|
||||
- `docs/zh-CN/godot/index.md`
|
||||
- `docs/zh-CN/tutorials/godot-integration.md`
|
||||
- `docs/zh-CN/source-generators/index.md`
|
||||
- `docs/zh-CN/api-reference/index.md`
|
||||
- `GFramework.Godot/README.md`
|
||||
- `.agents/skills/gframework-doc-refresh/SKILL.md`
|
||||
- 同时执行 `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot`,确认当前 `Godot`
|
||||
docs surface 除 `index.md`、`architecture.md`、`scene.md`、`ui.md`、`signal.md`、`extensions.md`、`logging.md`
|
||||
外,还应把 `storage.md` 与 `setting.md` 视为默认恢复集合的一部分
|
||||
- 巡检结论:
|
||||
- 根 `README.md`、`docs/zh-CN/godot/index.md`、`docs/zh-CN/tutorials/godot-integration.md`、
|
||||
`docs/zh-CN/source-generators/index.md` 与 `docs/zh-CN/api-reference/index.md` 当前仍保持同一套 `Godot`
|
||||
owner / adoption path 叙述,没有发现新的入口漂移
|
||||
- 本轮不需要改动稳定的 README / docs 页面,只需要把 active topic 的最小恢复摘要补齐到当前 landing page
|
||||
实际覆盖的页集合
|
||||
- 因此本轮执行的唯一修改是:
|
||||
- 更新 `ai-plan/public/documentation-full-coverage-governance/todos/documentation-full-coverage-governance-tracking.md`
|
||||
的恢复点、`Godot` 页面集合、稳定性巡检结论与下一步
|
||||
- 记录本条 `RP-013` trace,保证未来 `boot` 不会漏掉 `storage.md` / `setting.md`
|
||||
|
||||
### 当前决策(RP-013)
|
||||
|
||||
- 当前 topic 继续保持“巡检优先、最小修复”的节奏;验证通过时不为凑改动而重写稳定页面
|
||||
- `scan_module_evidence.py` 识别出的 docs surface 应优先反映到 active recovery artifact,而不是只留在一次性 chat
|
||||
上下文
|
||||
- `Godot` family 的后续巡检重点从“根入口是否还残留旧描述”切换为“storage / setting 子页是否和 landing / README
|
||||
保持同一口径”
|
||||
|
||||
### 当前验证(RP-013)
|
||||
|
||||
- 模块扫描:
|
||||
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot`:通过
|
||||
- 文档校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 若后续分支继续调整 `GFramework.Godot` 运行时入口,优先复核 `docs/zh-CN/godot/storage.md`、`setting.md` 与根
|
||||
`README.md` / landing page 是否仍保持同一套职责边界
|
||||
2. 当后续分支再修改 `Godot` / `Game` family 的 README、docs 或公共 API 时,回到对应模块追加 targeted 巡检与验证
|
||||
|
||||
### 当前恢复点:RP-016
|
||||
|
||||
- 用户明确要求“继续下一步的文档治理,并形成足够体量的 PR”后,当前 topic 不再停留在 validation-only 巡检,
|
||||
而是切到一个可独立成波次的 `Game` persistence docs surface:
|
||||
- `docs/zh-CN/game/data.md`
|
||||
- `docs/zh-CN/game/storage.md`
|
||||
- `docs/zh-CN/game/serialization.md`
|
||||
- `docs/zh-CN/game/setting.md`
|
||||
- 先执行 `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Game`,确认 `Game` 的默认 docs surface
|
||||
包含 `data`、`storage`、`serialization`、`setting`、`scene`、`ui`、`config-system` 与 landing / API fallback
|
||||
- 结合 `GFramework.Game/README.md`、`FileStorage.cs`、`ScopedStorage.cs`、`DataRepository.cs`、
|
||||
`UnifiedSettingsDataRepository.cs`、`SaveRepository.cs`、`JsonSerializer.cs`、`SettingsModel.cs`、
|
||||
`SettingsSystem.cs`、`JsonSerializerTests.cs`、`SettingsModelTests.cs` 与 `PersistenceTests.cs`,确认四个旧页面存在持续性漂移:
|
||||
- `storage.md` 仍按旧版通用 API 手册组织,没有强调 `FileStorage` / `ScopedStorage` 与 repository 的职责边界
|
||||
- `data.md` 缺少 `DataRepository`、`UnifiedSettingsDataRepository` 与 `SaveRepository<TSaveData>` 三层分工,以及当前备份 /
|
||||
批量事件 / 存档迁移语义
|
||||
- `serialization.md` 仍沿用“业务层手工 Serialize 再写回 storage”的旧接法,没有反映当前 `FileStorage`
|
||||
已直接复用注入的 `ISerializer`
|
||||
- `setting.md` 虽已回到 `ISettingsModel` / `RegisterApplicator(...)` 口径,但结构仍未与当前 `Game` family 的 runtime topic
|
||||
页面统一,也缺少 frontmatter
|
||||
- 因此本轮执行的最小但成组修复集是:
|
||||
- 重写 `docs/zh-CN/game/storage.md`
|
||||
- 重写 `docs/zh-CN/game/data.md`
|
||||
- 重写 `docs/zh-CN/game/serialization.md`
|
||||
- 重写 `docs/zh-CN/game/setting.md`
|
||||
- 更新 active tracking 的恢复点、治理结论与下一步
|
||||
|
||||
### 当前决策(RP-016)
|
||||
|
||||
- 这轮不去扩张到 `Game` tutorial 或 root README,而是把同一子领域里仍残留的旧页一次性收口,形成清晰的 PR 边界
|
||||
- `Game` persistence docs surface 统一采用“当前公开入口 -> 最小接入路径 -> 当前边界 -> 继续阅读”的结构,
|
||||
不再维护分散的伪 API 手册页
|
||||
- 文档示例只保留可直接映射到当前框架类型、测试行为或已验证 consumer wiring 的内容,避免继续写虚构接线名
|
||||
|
||||
### 当前验证(RP-016)
|
||||
|
||||
- 模块扫描:
|
||||
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Game`:通过
|
||||
- 文档校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/data.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/storage.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/serialization.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/setting.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
- 代码 / 测试证据:
|
||||
- `GFramework.Game/README.md`
|
||||
- `GFramework.Game/Storage/FileStorage.cs`
|
||||
- `GFramework.Game/Storage/ScopedStorage.cs`
|
||||
- `GFramework.Game/Data/DataRepository.cs`
|
||||
- `GFramework.Game/Data/UnifiedSettingsDataRepository.cs`
|
||||
- `GFramework.Game/Data/SaveRepository.cs`
|
||||
- `GFramework.Game/Serializer/JsonSerializer.cs`
|
||||
- `GFramework.Game/Setting/SettingsModel.cs`
|
||||
- `GFramework.Game/Setting/SettingsSystem.cs`
|
||||
- `GFramework.Game.Tests/Data/PersistenceTests.cs`
|
||||
- `GFramework.Game.Tests/Serializer/JsonSerializerTests.cs`
|
||||
- `GFramework.Game.Tests/Setting/SettingsModelTests.cs`
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 回填 tracking 的最新验证结果,并按仓库规则提交本轮 `Game` persistence docs wave
|
||||
2. 若后续分支继续调整 `GFramework.Game` 的 persistence runtime 或 README,优先复核这四个 topic page 与 landing page 的一致性
|
||||
|
||||
### 当前恢复点:RP-015
|
||||
|
||||
- 通过 `$gframework-boot` 恢复当前 worktree 后,继续按 `documentation-full-coverage-governance` 的默认下一步执行一次
|
||||
validation-only 巡检,并补读:
|
||||
- `GFramework.Godot/README.md`
|
||||
- `docs/zh-CN/godot/index.md`
|
||||
- `docs/zh-CN/godot/storage.md`
|
||||
- `docs/zh-CN/godot/setting.md`
|
||||
- `docs/zh-CN/source-generators/index.md`
|
||||
- `docs/zh-CN/api-reference/index.md`
|
||||
- `docs/zh-CN/tutorials/godot-integration.md`
|
||||
- `GFramework.Godot/Setting/GodotAudioSettings.cs`
|
||||
- `GFramework.Godot/Setting/GodotGraphicsSettings.cs`
|
||||
- `GFramework.Godot/Setting/GodotLocalizationSettings.cs`
|
||||
- `GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs`
|
||||
- 同时重新执行 `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot`,确认 `Godot`
|
||||
docs surface 仍然覆盖 landing、`storage.md`、`setting.md`、source-generators fallback、API reference 与
|
||||
CoreGrid 参考接线,没有新的默认恢复页缺口
|
||||
- 巡检结论:
|
||||
- `GFramework.Godot/README.md`、`docs/zh-CN/godot/index.md`、`storage.md`、`setting.md`、`docs/zh-CN/source-generators/index.md`、
|
||||
`docs/zh-CN/api-reference/index.md` 与 `docs/zh-CN/tutorials/godot-integration.md` 当前仍保持同一套
|
||||
`Godot` owner / adoption path 叙述,没有发现自 `RP-014` 之后的新入口漂移
|
||||
- `setting.md` 里关于 `ISettingsModel`、`RegisterApplicator(...)`、`LocalizationMap` fallback 的描述,仍与
|
||||
`GodotAudioSettings`、`GodotGraphicsSettings`、`GodotLocalizationSettings` 以及
|
||||
`GodotLocalizationSettingsTests` 保持一致
|
||||
- 因此本轮执行的唯一修改是:
|
||||
- 更新 active tracking 与 trace 的恢复点、巡检结论和验证结果
|
||||
|
||||
### 当前决策(RP-015)
|
||||
|
||||
- 当前 topic 继续维持“巡检优先、无漂移则只更新 recovery artifact”的治理节奏,不为凑改动重写稳定页面
|
||||
- `boot` 恢复本身也应留下可复用的恢复证据,避免下一次启动时重复判断“最近一次巡检是否已经覆盖 `storage.md` /
|
||||
`setting.md`”
|
||||
|
||||
### 当前验证(RP-015)
|
||||
|
||||
- 模块扫描:
|
||||
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot`:通过
|
||||
- 文档校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 若后续分支继续调整 `GFramework.Godot` 运行时入口,优先复核 `docs/zh-CN/godot/storage.md`、`setting.md` 与根
|
||||
`README.md` / landing page 是否仍保持同一套职责边界
|
||||
2. 当后续分支再修改 `Godot` / `Game` family 的 README、docs 或公共 API 时,回到对应模块追加 targeted 巡检与验证
|
||||
|
||||
### 当前恢复点:RP-014
|
||||
|
||||
- 继续沿用 `RP-013` 的 `Godot` docs surface 巡检范围,补读:
|
||||
- `docs/zh-CN/godot/storage.md`
|
||||
- `docs/zh-CN/godot/setting.md`
|
||||
- `GFramework.Godot/Storage/GodotFileStorage.cs`
|
||||
- `GFramework.Godot/Setting/GodotAudioSettings.cs`
|
||||
- `GFramework.Godot/Setting/GodotGraphicsSettings.cs`
|
||||
- `GFramework.Godot/Setting/GodotLocalizationSettings.cs`
|
||||
- `GFramework.Godot/Setting/Data/AudioBusMap.cs`
|
||||
- `GFramework.Godot/Setting/Data/LocalizationMap.cs`
|
||||
- `GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs`
|
||||
- `ai-libs/CoreGrid/scripts/module/UtilityModule.cs`
|
||||
- `ai-libs/CoreGrid/scripts/module/ModelModule.cs`
|
||||
- 巡检发现两处新的 topic 级漂移:
|
||||
- `docs/zh-CN/godot/storage.md` 仍按旧版 API 手册组织,缺少 frontmatter、当前 `IStorage` / repository 分工与
|
||||
`GodotYamlConfigLoader` 分流说明
|
||||
- `docs/zh-CN/godot/setting.md` 仍使用过时的“settings data 直接注入 applicator 构造函数”叙述,没有反映当前
|
||||
`ISettingsModel` + `RegisterApplicator(...)` 的真实接线方式
|
||||
- 因此本轮执行最小修复集:
|
||||
- 重写 `docs/zh-CN/godot/storage.md`
|
||||
- 重写 `docs/zh-CN/godot/setting.md`
|
||||
- 更新 active tracking 的恢复点、巡检结论与验证结果
|
||||
|
||||
### 当前决策(RP-014)
|
||||
|
||||
- 继续遵循“README / landing 稳定时,不重写稳定入口;只修新发现的 topic 漂移”的治理节奏
|
||||
- `storage.md` 应强调宿主路径语义与 repository 分工,而不是重复 `Game` 通用存储手册
|
||||
- `setting.md` 应强调 applicator 注册和运行时边界,而不是重新维护一份过时的设置 API 摘要
|
||||
|
||||
### 当前验证(RP-014)
|
||||
|
||||
- 模块扫描:
|
||||
- `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Godot`:通过
|
||||
- 文档校验:
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`:通过
|
||||
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`:通过
|
||||
- 构建校验:
|
||||
- `cd docs && bun run build`:通过;仅保留既有 VitePress 大 chunk warning,无构建失败
|
||||
|
||||
### 下一步
|
||||
|
||||
1. 若后续分支继续调整 `GFramework.Godot` 运行时入口,优先复核 `docs/zh-CN/godot/storage.md`、`setting.md` 与根
|
||||
`README.md` / landing page 是否仍保持同一套职责边界
|
||||
2. 当后续分支再修改 `Godot` / `Game` family 的 README、docs 或公共 API 时,回到对应模块追加 targeted 巡检与验证
|
||||
|
||||
@ -92,12 +92,3 @@ var options = new ArchOptions
|
||||
3. 回到对应模块 README:
|
||||
- `GFramework.Ecs.Arch.Abstractions/README.md`
|
||||
- `GFramework.Ecs.Arch/README.md`
|
||||
|
||||
## 边界提醒
|
||||
|
||||
- `GFramework.Core.SourceGenerators.Abstractions`
|
||||
- `GFramework.Godot.SourceGenerators.Abstractions`
|
||||
- `GFramework.SourceGenerators.Common`
|
||||
|
||||
这些目录当前都不是独立消费模块,而是源码生成器家族的内部支撑组件。它们不属于抽象接口栏目里的独立采用入口,
|
||||
应分别跟随 `Core.SourceGenerators`、`Godot.SourceGenerators` 或其他生成器模块的 README 与专题页维护。
|
||||
|
||||
@ -1,94 +1,122 @@
|
||||
# Game Abstractions
|
||||
|
||||
> GFramework.Game.Abstractions 游戏模块抽象接口定义
|
||||
|
||||
## 概述
|
||||
|
||||
GFramework.Game.Abstractions 包含了游戏特定功能的抽象接口,这些接口定义了游戏开发中的通用契约。
|
||||
|
||||
## 存档接口
|
||||
|
||||
### ISaveSystem
|
||||
|
||||
存档系统接口:
|
||||
|
||||
```csharp
|
||||
public interface ISaveSystem
|
||||
{
|
||||
void Save(string slotId, SaveData data);
|
||||
SaveData Load(string slotId);
|
||||
bool HasSave(string slotId);
|
||||
void Delete(string slotId);
|
||||
List<SaveSlotInfo> GetAllSaveSlots();
|
||||
}
|
||||
```
|
||||
|
||||
### ISaveData
|
||||
|
||||
存档数据接口:
|
||||
|
||||
```csharp
|
||||
public interface ISaveData
|
||||
{
|
||||
int Version { get; }
|
||||
DateTime Timestamp { get; }
|
||||
void Validate();
|
||||
}
|
||||
```
|
||||
|
||||
## 设置接口
|
||||
|
||||
### IGameSettings
|
||||
|
||||
游戏设置接口:
|
||||
|
||||
```csharp
|
||||
public interface IGameSettings
|
||||
{
|
||||
AudioSettings Audio { get; }
|
||||
GraphicsSettings Graphics { get; }
|
||||
InputSettings Input { get; }
|
||||
|
||||
void Save();
|
||||
void Load();
|
||||
void ResetToDefaults();
|
||||
}
|
||||
```
|
||||
|
||||
## 场景管理接口
|
||||
|
||||
### ISceneManager
|
||||
|
||||
场景管理器接口:
|
||||
|
||||
```csharp
|
||||
public interface ISceneManager
|
||||
{
|
||||
void SwitchScene<TScene>() where TScene : IScene;
|
||||
Task SwitchSceneAsync<TScene>() where TScene : IScene;
|
||||
|
||||
void PushScene<TScene>() where TScene : IScene;
|
||||
void PopScene();
|
||||
|
||||
IScene CurrentScene { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### IScene
|
||||
|
||||
场景接口:
|
||||
|
||||
```csharp
|
||||
public interface IScene
|
||||
{
|
||||
void OnEnter();
|
||||
void OnExit();
|
||||
void OnUpdate(float delta);
|
||||
}
|
||||
```
|
||||
---
|
||||
title: Game Abstractions
|
||||
description: GFramework.Game.Abstractions 的契约边界、包关系与 XML 阅读重点。
|
||||
---
|
||||
|
||||
**相关文档**:
|
||||
# Game Abstractions
|
||||
|
||||
- [Game 概述](../game/index.md)
|
||||
- [核心抽象](./core-abstractions.md)
|
||||
`GFramework.Game.Abstractions` 是 `Game` 运行时的契约包。
|
||||
|
||||
它建立在 `GFramework.Core.Abstractions` 之上,负责定义配置、数据、设置、场景、UI、路由、存储和资源注册表相关的接口、
|
||||
枚举与事件契约;默认实现、路由基类、YAML 加载器、文件存储和设置 / 存档仓库则在 `GFramework.Game` 中。
|
||||
|
||||
如果你要开箱即用地接入游戏运行时能力,应依赖 `GFramework.Game`;如果你在做共享业务层、feature 包、测试替身或引擎适配层,
|
||||
才单独消费本包。
|
||||
|
||||
## 什么时候单独依赖它
|
||||
|
||||
- 你希望公共业务层只依赖 `ISceneRouter`、`IUiRouter`、`ISettingsSystem`、`ISaveRepository<TSaveData>` 这类契约
|
||||
- 你要让多个程序集共享 `ISettingsData`、`IData`、`ISceneEnterParam`、`IUiPageEnterParam` 等数据和路由上下文
|
||||
- 你需要自己实现 factory、root、存储或配置加载器,但不想把默认运行时一起带进来
|
||||
|
||||
## 包关系
|
||||
|
||||
- 契约层:`GFramework.Game.Abstractions`
|
||||
- 运行时实现:`GFramework.Game`
|
||||
- 底层基础契约:`GFramework.Core.Abstractions`
|
||||
|
||||
## 契约地图
|
||||
|
||||
| 契约族 | 作用 |
|
||||
| --- | --- |
|
||||
| `Config/` | `IConfigLoader`、`IConfigRegistry`、`IConfigTable<TKey, TValue>` 和配置失败诊断模型 |
|
||||
| `Data/` | `IData`、`IVersionedData`、`IDataRepository`、`ISettingsDataRepository`、`ISaveRepository<TSaveData>` 及数据事件 |
|
||||
| `Setting/` | `ISettingsData`、`ISettingsModel`、`ISettingsSystem`、设置迁移契约与内置设置数据类型 |
|
||||
| `Scene/` | `IScene`、`ISceneRouter`、`ISceneFactory`、`ISceneRoot`、转场处理器与事件 |
|
||||
| `UI/` | `IUiPage`、`IUiRouter`、`IUiFactory`、`IUiRoot`、交互配置与 UI 转场选项 |
|
||||
| `Routing/` | `IRoute`、`IRouteContext`、`IRouteGuard<TRoute>`,作为 Scene / UI 共享的路由基础约定 |
|
||||
| `Storage/` `Asset/` `Enums/` | 文件存储角色、资源注册表,以及转场 / UI 层级 / 输入动作等跨层枚举 |
|
||||
|
||||
## XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-23` 对 `GFramework.Game.Abstractions` 做的一轮轻量 XML 盘点结果:只统计公开 /
|
||||
内部类型声明是否带 XML 注释,用来建立契约层阅读入口;成员级参数、返回值、异常和生命周期说明仍需要后续 API 波次继续细化。
|
||||
|
||||
| 契约族 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Config/` | `7/7` 个类型声明已带 XML 注释 | `IConfigLoader`、`IConfigRegistry`、`IConfigTable<TKey, TValue>`、`ConfigLoadException` | 看配置表注册、只读访问和失败诊断边界 |
|
||||
| `Data/` | `14/14` 个类型声明已带 XML 注释 | `IDataRepository`、`ISettingsDataRepository`、`ISaveRepository<TSaveData>`、`DataRepositoryOptions` | 看业务数据、统一设置文件、槽位存档与迁移契约 |
|
||||
| `Setting/` | `12/12` 个类型声明已带 XML 注释 | `ISettingsData`、`ISettingsModel`、`ISettingsSystem`、`LocalizationSettings` | 看设置生命周期、应用语义、迁移接口和内置设置对象 |
|
||||
| `Scene/` | `14/14` 个类型声明已带 XML 注释 | `IScene`、`ISceneRouter`、`ISceneFactory`、`SceneTransitionEvent` | 看场景行为、工厂 / root 边界和转场模型 |
|
||||
| `UI/` | `19/19` 个类型声明已带 XML 注释 | `IUiPage`、`IUiRouter`、`IUiFactory`、`UiInteractionProfile`、`UiTransitionHandlerOptions` | 看页面栈、层级 UI、输入动作和 UI 转场契约 |
|
||||
| `Routing/` `Storage/` `Asset/` `Enums/` | `13/13` 个类型声明已带 XML 注释 | `IRoute`、`IRouteContext`、`IFileStorage`、`IAssetRegistry<T>`、`UiLayer`、`SceneTransitionType` | 看公共路由上下文、存储角色、资源注册表与共享枚举 |
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 1. 只在公共业务层声明游戏对象
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
|
||||
public sealed class GameSaveData : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public DateTime LastModified { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class GameplayEnterParam : ISceneEnterParam
|
||||
{
|
||||
public required string Seed { get; init; }
|
||||
}
|
||||
|
||||
public sealed class PauseMenuParam : IUiPageEnterParam
|
||||
{
|
||||
public bool AllowRestart { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 让 feature 包只依赖抽象
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
|
||||
public sealed class ContinueGameCommandHandler
|
||||
{
|
||||
private readonly ISaveRepository<GameSaveData> _saveRepository;
|
||||
private readonly ISceneRouter _sceneRouter;
|
||||
|
||||
public ContinueGameCommandHandler(
|
||||
ISaveRepository<GameSaveData> saveRepository,
|
||||
ISceneRouter sceneRouter)
|
||||
{
|
||||
_saveRepository = saveRepository;
|
||||
_sceneRouter = sceneRouter;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 什么时候切到运行时包
|
||||
|
||||
下面这些需求都属于 `GFramework.Game` 的职责,而不是本包:
|
||||
|
||||
- 使用默认的 `JsonSerializer`、`FileStorage` 或 `ScopedStorage`
|
||||
- 使用 `SettingsModel<TRepository>`、`SettingsSystem`、`SaveRepository<TSaveData>` 等默认实现
|
||||
- 使用 `YamlConfigLoader`、`GameConfigBootstrap`、`GameConfigModule`
|
||||
- 继承 `SceneRouterBase`、`UiRouterBase` 或默认转场处理器基类
|
||||
|
||||
## 阅读顺序
|
||||
|
||||
1. 先读本页,确认你是否真的只需要契约层
|
||||
2. 再看 [`../game/index.md`](../game/index.md) 了解默认运行时怎么组织这些契约
|
||||
3. 继续读具体专题页:
|
||||
- [`../game/config-system.md`](../game/config-system.md)
|
||||
- [`../game/data.md`](../game/data.md)
|
||||
- [`../game/setting.md`](../game/setting.md)
|
||||
- [`../game/scene.md`](../game/scene.md)
|
||||
- [`../game/ui.md`](../game/ui.md)
|
||||
4. 需要仓库侧入口时,回到:
|
||||
- `GFramework.Game.Abstractions/README.md`
|
||||
- `GFramework.Game/README.md`
|
||||
|
||||
@ -32,7 +32,7 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
|
||||
| `Core` / `Core.Abstractions` | `GFramework.Core/README.md`、`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`、`GFramework.Cqrs.Abstractions/README.md`、`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`、`GFramework.Game.Abstractions/README.md`、`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`、`GFramework.Godot.SourceGenerators/README.md` | [`../godot/index.md`](../godot/index.md)、[`../source-generators/index.md`](../source-generators/index.md) | 节点扩展、场景 / UI 适配、资源 / 存储 / 日志接入 |
|
||||
| `Godot` / `Godot.SourceGenerators` | `GFramework.Godot/README.md`、`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`、`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,还是先看教程
|
||||
|
||||
@ -131,6 +131,6 @@ public sealed class GameLoop
|
||||
|
||||
## 边界说明
|
||||
|
||||
- 当前仓库没有交付其他可直接消费的 ECS 运行时包;旧文档里把“可能支持的其他 ECS 框架”写成现有能力会误导采用路径。
|
||||
- 当前仓库没有交付其他可直接消费的 ECS 运行时包;旧文档把“未来可能支持的其他 ECS 框架”写成现有能力,会误导采用路径。
|
||||
- `GFramework.Ecs.Arch.Abstractions` 负责“边界”,`GFramework.Ecs.Arch` 负责“默认实现”。
|
||||
- 站内页面只维护可构建的 docs 链路;仓库根 README 和模块 README 继续承担包目录入口职责。
|
||||
|
||||
@ -1,709 +1,189 @@
|
||||
---
|
||||
title: 数据与存档系统
|
||||
description: 数据与存档系统提供统一的数据持久化基础能力,支持多槽位存档、版本化数据和仓库抽象。
|
||||
description: 以当前 GFramework.Game 源码与 PersistenceTests 为准,说明 DataRepository、UnifiedSettingsDataRepository 和 SaveRepository 的职责边界。
|
||||
---
|
||||
|
||||
# 数据与存档系统
|
||||
|
||||
## 概述
|
||||
`GFramework.Game` 的数据持久化不是“只有一个万能仓库”。
|
||||
|
||||
数据与存档系统是 GFramework.Game 中用于管理游戏数据持久化的核心组件。它提供了统一的数据加载和保存接口,支持多槽位存档管理、版本化数据模式,以及与存储系统、序列化系统的组合使用。
|
||||
当前更准确的理解是三层分工:
|
||||
|
||||
通过数据系统,你可以将游戏数据保存到本地存储,支持多个存档槽位,并在需要时于应用层实现版本迁移。
|
||||
- `DataRepository`
|
||||
- 面向“一个 location 对应一份持久化对象”的通用数据仓库
|
||||
- `UnifiedSettingsDataRepository`
|
||||
- 面向“多个设置 section 聚合到同一个文件”的设置仓库
|
||||
- `SaveRepository<TSaveData>`
|
||||
- 面向“按槽位组织的版本化存档”
|
||||
|
||||
**主要特性**:
|
||||
如果先把这三类入口分开理解,后续采用路径会清晰很多。
|
||||
|
||||
- 统一的数据持久化接口
|
||||
- 多槽位存档管理
|
||||
- 数据版本控制模式
|
||||
- 异步加载和保存
|
||||
- 批量数据操作
|
||||
- 与存储系统集成
|
||||
## 什么时候用哪个仓库
|
||||
|
||||
## 核心概念
|
||||
### `DataRepository`
|
||||
|
||||
### 数据接口
|
||||
适合:
|
||||
|
||||
`IData` 标记数据类型:
|
||||
- 单份玩家档案
|
||||
- 单份运行时缓存
|
||||
- 一条 location 对应一个文件的普通业务数据
|
||||
|
||||
默认语义是:
|
||||
|
||||
- `IDataLocation` 决定 key
|
||||
- 一条 location 对应一份对象
|
||||
- 覆盖保存时可按 `DataRepositoryOptions.AutoBackup` 创建 `<key>.backup`
|
||||
- `SaveAllAsync(...)` 视为一次批量提交,只发送批量事件,不重复发送单项保存事件
|
||||
|
||||
### `UnifiedSettingsDataRepository`
|
||||
|
||||
适合:
|
||||
|
||||
- 音频、图形、语言等多个设置 section 统一落到一份文件
|
||||
- 启动时一次性加载所有设置,再交给 `SettingsModel<TRepository>` 编排
|
||||
|
||||
默认语义是:
|
||||
|
||||
- 底层持久化文件只有一份,默认文件名是 `settings.json`
|
||||
- 各个设置 section 仍然通过 `IDataLocation` 的 key 区分
|
||||
- 保存、删除时会整文件回写,而不是只改单个 section 文件
|
||||
- 开启 `AutoBackup` 时,备份粒度也是整个统一文件,不是单个 section
|
||||
|
||||
### `SaveRepository<TSaveData>`
|
||||
|
||||
适合:
|
||||
|
||||
- 多槽位存档
|
||||
- 需要版本迁移的 save data
|
||||
- 需要列举现有槽位和删除槽位
|
||||
|
||||
默认语义是:
|
||||
|
||||
- 按 `SaveRoot` / `SaveSlotPrefix` / `SaveFileName` 组织目录
|
||||
- 槽位不存在时,`LoadAsync(slot)` 返回新的 `TSaveData` 实例,而不是 `null`
|
||||
- `ListSlotsAsync()` 只返回真实存在存档文件的槽位,并按升序排列
|
||||
- 迁移成功后会把升级后的结果自动回写到槽位文件
|
||||
|
||||
## 当前公开入口
|
||||
|
||||
### `DataRepository`
|
||||
|
||||
`DataRepository` 是最通用的默认实现。当前仓库和测试确认的行为有几条需要特别记住:
|
||||
|
||||
- `LoadAsync<T>(location)` 在文件不存在时返回 `new T()`,不是抛异常
|
||||
- `DeleteAsync(location)` 只有在目标数据真实存在并被删除时才发送删除事件
|
||||
- `SaveAllAsync(...)` 会抑制逐项 `DataSavedEvent<T>`,只保留一次 `DataBatchSavedEvent`
|
||||
- `AutoBackup = true` 时,覆盖旧值前会先把旧值写到 `<key>.backup`
|
||||
|
||||
最小接法通常是:项目先准备一个 `IDataLocation` 或 `IDataLocationProvider`,再把它交给 `DataRepository` 做
|
||||
`location -> key` 的映射;repository 自己不负责推导业务对象应该落在哪个位置。
|
||||
|
||||
### `UnifiedSettingsDataRepository`
|
||||
|
||||
当前 `SettingsModel<TRepository>` 依赖的默认设置仓库就是它。
|
||||
|
||||
它和普通 `DataRepository` 的关键区别不是接口,而是落盘形态:
|
||||
|
||||
- `DataRepository`
|
||||
- 每个 location 对应一个独立文件
|
||||
- `UnifiedSettingsDataRepository`
|
||||
- 所有 section 聚合到同一个统一文件
|
||||
|
||||
还有两个容易遗漏的点:
|
||||
|
||||
- `LoadAllAsync()` 依赖 `RegisterDataType(location, type)` 建立 section -> 运行时类型映射
|
||||
- 仓库内部会先把统一文件加载进缓存,再在保存 / 删除时基于快照整文件提交
|
||||
|
||||
这就是为什么 `SettingsModel<TRepository>` 会在拿到 `GetData<T>()` 或 `RegisterApplicator(...)` 后主动把类型注册回 repository。
|
||||
|
||||
### `SaveRepository<TSaveData>`
|
||||
|
||||
`SaveRepository<TSaveData>` 用于槽位存档,不直接复用 `IDataLocation`。
|
||||
|
||||
最重要的公开配置是 `SaveConfiguration`:
|
||||
|
||||
```csharp
|
||||
public interface IData
|
||||
var config = new SaveConfiguration
|
||||
{
|
||||
// 标记接口,用于标识可持久化的数据
|
||||
}
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save.json"
|
||||
};
|
||||
```
|
||||
|
||||
### 数据仓库
|
||||
按这个配置,槽位 `1` 的默认文件结构就是:
|
||||
|
||||
`IDataRepository` 提供通用的数据操作:
|
||||
|
||||
```csharp
|
||||
public interface IDataRepository : IUtility
|
||||
{
|
||||
Task<T> LoadAsync<T>(IDataLocation location) where T : class, IData, new();
|
||||
Task SaveAsync<T>(IDataLocation location, T data) where T : class, IData;
|
||||
Task<bool> ExistsAsync(IDataLocation location);
|
||||
Task DeleteAsync(IDataLocation location);
|
||||
Task SaveAllAsync(IEnumerable<(IDataLocation, IData)> dataList);
|
||||
}
|
||||
```text
|
||||
saves/
|
||||
slot_1/
|
||||
save.json
|
||||
```
|
||||
|
||||
`IDataRepository` 描述的是“仓库语义”,不是固定的落盘格式。实现可以选择每个数据项独立成文件,也可以把多个 section 聚合到同一个文件里。
|
||||
当前实现内部会先把根存储包装成 `ScopedStorage(storage, config.SaveRoot)`,再按槽位继续加前缀,因此项目层一般不需要手工再拼一次 `"saves/slot_1"`。
|
||||
|
||||
当前内建实现里:
|
||||
## 存档迁移的真实语义
|
||||
|
||||
- `DataRepository` 采用“每个 location 一份持久化对象”的模型
|
||||
- `UnifiedSettingsDataRepository` 采用“所有设置聚合到一个统一文件”的模型
|
||||
`SaveRepository<TSaveData>` 只有在 `TSaveData` 实现了 `IVersionedData` 时,才支持 `RegisterMigration(...)`。
|
||||
|
||||
两者对外遵守同一套约定:
|
||||
当前源码和 `PersistenceTests` 明确约束了下面这些行为:
|
||||
|
||||
- `SaveAllAsync(...)` 视为一次批量提交,只发送 `DataBatchSavedEvent`,不会再为每个条目重复发送 `DataSavedEvent<T>`
|
||||
- `DeleteAsync(...)` 只有在目标数据真实存在并被删除时才会发送删除事件
|
||||
- 当 `DataRepositoryOptions.AutoBackup = true` 时,覆盖已有数据前会先保留上一份快照
|
||||
- 对 `UnifiedSettingsDataRepository` 来说,备份粒度是整个统一文件,而不是单个设置 section
|
||||
- 非版本化 save type 注册迁移器会直接失败
|
||||
- 同一个 `FromVersion` 不能重复注册迁移器
|
||||
- 迁移链缺口会显式抛错,不会静默返回半升级结果
|
||||
- 迁移器声明的 `ToVersion` 必须与实际返回对象的版本一致
|
||||
- 如果读到比当前运行时代码更高版本的存档,也会明确失败
|
||||
- 单次加载会先固定一份迁移表快照,避免并发注册让同一次加载看到变化中的链路
|
||||
|
||||
### 存档仓库
|
||||
也就是说,`SaveRepository<TSaveData>` 的迁移语义更偏“严格升级管线”,而不是“尽量帮你读出来”。
|
||||
|
||||
`ISaveRepository<T>` 专门用于管理游戏存档:
|
||||
|
||||
```csharp
|
||||
public interface ISaveRepository<TSaveData> : IUtility
|
||||
where TSaveData : class, IData, new()
|
||||
{
|
||||
ISaveRepository<TSaveData> RegisterMigration(ISaveMigration<TSaveData> migration);
|
||||
Task<bool> ExistsAsync(int slot);
|
||||
Task<TSaveData> LoadAsync(int slot);
|
||||
Task SaveAsync(int slot, TSaveData data);
|
||||
Task DeleteAsync(int slot);
|
||||
Task<IReadOnlyList<int>> ListSlotsAsync();
|
||||
}
|
||||
```
|
||||
|
||||
`ISaveMigration<TSaveData>` 定义单步迁移:
|
||||
|
||||
```csharp
|
||||
public interface ISaveMigration<TSaveData>
|
||||
where TSaveData : class, IData
|
||||
{
|
||||
int FromVersion { get; }
|
||||
int ToVersion { get; }
|
||||
TSaveData Migrate(TSaveData oldData);
|
||||
}
|
||||
```
|
||||
|
||||
### 版本化数据
|
||||
|
||||
`IVersionedData` 支持数据版本管理:
|
||||
|
||||
```csharp
|
||||
public interface IVersionedData : IData
|
||||
{
|
||||
int Version { get; }
|
||||
DateTime LastModified { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义数据类型
|
||||
## 最小接入路径
|
||||
|
||||
下面是当前 `Game` 层最常见的一套组合方式:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Serializer;
|
||||
using GFramework.Core.Abstractions.Storage;
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
|
||||
// 简单数据
|
||||
public class PlayerData : IData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; }
|
||||
}
|
||||
|
||||
// 版本化数据
|
||||
public class SaveData : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public PlayerData Player { get; set; }
|
||||
public DateTime SaveTime { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 使用存档仓库
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Controller;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
[ContextAware]
|
||||
public partial class SaveController : IController
|
||||
{
|
||||
public async Task SaveGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 创建存档数据
|
||||
var saveData = new SaveData
|
||||
{
|
||||
Player = new PlayerData
|
||||
{
|
||||
Name = "Player1",
|
||||
Level = 10,
|
||||
Experience = 1000
|
||||
},
|
||||
SaveTime = DateTime.Now
|
||||
};
|
||||
|
||||
// 保存到指定槽位
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
Console.WriteLine($"游戏已保存到槽位 {slot}");
|
||||
}
|
||||
|
||||
public async Task LoadGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 检查存档是否存在
|
||||
if (!await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
Console.WriteLine($"槽位 {slot} 不存在存档");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载存档
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
Console.WriteLine($"加载存档: {saveData.Player.Name}, 等级 {saveData.Player.Level}");
|
||||
}
|
||||
|
||||
public async Task DeleteSave(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 删除存档
|
||||
await saveRepo.DeleteAsync(slot);
|
||||
Console.WriteLine($"已删除槽位 {slot} 的存档");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册存档仓库
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Data;
|
||||
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 获取存储系统
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
// 创建存档配置
|
||||
var saveConfig = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save.json"
|
||||
};
|
||||
|
||||
// 注册存档仓库
|
||||
var saveRepo = new SaveRepository<SaveData>(storage, saveConfig);
|
||||
RegisterUtility<ISaveRepository<SaveData>>(saveRepo);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 列出所有存档
|
||||
|
||||
```csharp
|
||||
public async Task ShowSaveList()
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 获取所有存档槽位
|
||||
var slots = await saveRepo.ListSlotsAsync();
|
||||
|
||||
Console.WriteLine($"找到 {slots.Count} 个存档:");
|
||||
foreach (var slot in slots)
|
||||
{
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
Console.WriteLine($"槽位 {slot}: {saveData.Player.Name}, " +
|
||||
$"等级 {saveData.Player.Level}, " +
|
||||
$"保存时间 {saveData.SaveTime}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自动保存
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Controller;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
[ContextAware]
|
||||
public partial class AutoSaveController : IController
|
||||
{
|
||||
private CancellationTokenSource? _autoSaveCts;
|
||||
|
||||
public void StartAutoSave(int slot, TimeSpan interval)
|
||||
{
|
||||
_autoSaveCts = new CancellationTokenSource();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!_autoSaveCts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(interval, _autoSaveCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await SaveGame(slot);
|
||||
Console.WriteLine("自动保存完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"自动保存失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}, _autoSaveCts.Token);
|
||||
}
|
||||
|
||||
public void StopAutoSave()
|
||||
{
|
||||
_autoSaveCts?.Cancel();
|
||||
_autoSaveCts?.Dispose();
|
||||
_autoSaveCts = null;
|
||||
}
|
||||
|
||||
private async Task SaveGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
var saveData = CreateSaveData();
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
}
|
||||
|
||||
private SaveData CreateSaveData()
|
||||
{
|
||||
// 从游戏状态创建存档数据
|
||||
return new SaveData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 数据版本迁移
|
||||
|
||||
`SaveRepository<TSaveData>` 现在支持注册正式的迁移器,并在 `LoadAsync(slot)` 时自动升级旧版本存档。
|
||||
|
||||
迁移规则如下:
|
||||
|
||||
- `TSaveData` 需要实现 `IVersionedData`
|
||||
- 仓库以 `new TSaveData().Version` 作为当前运行时目标版本
|
||||
- 每个迁移器负责一个 `FromVersion -> ToVersion` 跳转
|
||||
- 同一个 `FromVersion` 只能注册一个迁移器,且 `ToVersion` 必须严格大于 `FromVersion`
|
||||
- 加载时仓库会按链路连续执行迁移,并在成功后自动回写升级后的存档
|
||||
- 迁移器返回数据上的 `Version` 必须与声明的 `ToVersion` 一致
|
||||
- 如果缺少中间迁移器,或者读到了比当前运行时更高的版本,`LoadAsync` 会抛出异常,避免静默加载错误数据
|
||||
|
||||
```csharp
|
||||
public sealed class SaveData : IVersionedData
|
||||
{
|
||||
// 当前运行时代码支持的最新版本
|
||||
public int Version { get; set; } = 2;
|
||||
public string PlayerName { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SaveDataMigrationV1ToV2 : ISaveMigration<SaveData>
|
||||
{
|
||||
public int FromVersion => 1;
|
||||
|
||||
public int ToVersion => 2;
|
||||
|
||||
public SaveData Migrate(SaveData oldData)
|
||||
{
|
||||
return new SaveData
|
||||
{
|
||||
Version = 2,
|
||||
PlayerName = oldData.PlayerName,
|
||||
Level = oldData.Level,
|
||||
Experience = oldData.Level * 100,
|
||||
LastModified = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SaveModule : AbstractModule
|
||||
{
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
var storage = architecture.GetUtility<IStorage>();
|
||||
var saveConfig = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save"
|
||||
};
|
||||
|
||||
var saveRepo = new SaveRepository<SaveData>(storage, saveConfig)
|
||||
.RegisterMigration(new SaveDataMigrationV1ToV2());
|
||||
|
||||
architecture.RegisterUtility<ISaveRepository<SaveData>>(saveRepo);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SaveData> LoadGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 如果槽位里是 v1,仓库会自动迁移到 v2,并把新版本重新写回存储。
|
||||
return await saveRepo.LoadAsync(slot);
|
||||
}
|
||||
```
|
||||
|
||||
`ISaveMigration<TSaveData>` 接收和返回的是同一个存档类型。也就是说,框架提供的是“当前类型内的版本升级管线”,
|
||||
而不是跨 CLR 类型的双模型反序列化系统。如果旧版本缺失了新字段,反序列化会先使用当前类型的默认值,再由迁移器补齐。
|
||||
|
||||
### 使用数据仓库
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Controller;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
[ContextAware]
|
||||
public partial class SettingsController : IController
|
||||
{
|
||||
public async Task SaveSettings()
|
||||
{
|
||||
var dataRepo = this.GetUtility<IDataRepository>();
|
||||
|
||||
var settings = new GameSettings
|
||||
{
|
||||
MasterVolume = 0.8f,
|
||||
MusicVolume = 0.6f,
|
||||
SfxVolume = 0.7f
|
||||
};
|
||||
|
||||
// 定义数据位置
|
||||
var location = new DataLocation("settings", "game_settings.json");
|
||||
|
||||
// 保存设置
|
||||
await dataRepo.SaveAsync(location, settings);
|
||||
}
|
||||
|
||||
public async Task<GameSettings> LoadSettings()
|
||||
{
|
||||
var dataRepo = this.GetUtility<IDataRepository>();
|
||||
var location = new DataLocation("settings", "game_settings.json");
|
||||
|
||||
// 检查是否存在
|
||||
if (!await dataRepo.ExistsAsync(location))
|
||||
{
|
||||
return new GameSettings(); // 返回默认设置
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
return await dataRepo.LoadAsync<GameSettings>(location);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 批量保存数据
|
||||
|
||||
```csharp
|
||||
public async Task SaveAllGameData()
|
||||
{
|
||||
var dataRepo = this.GetUtility<IDataRepository>();
|
||||
|
||||
var dataList = new List<(IDataLocation, IData)>
|
||||
{
|
||||
(new DataLocation("player", "profile.json"), playerData),
|
||||
(new DataLocation("inventory", "items.json"), inventoryData),
|
||||
(new DataLocation("quests", "progress.json"), questData)
|
||||
};
|
||||
|
||||
// 批量保存
|
||||
await dataRepo.SaveAllAsync(dataList);
|
||||
Console.WriteLine("所有数据已保存");
|
||||
}
|
||||
```
|
||||
|
||||
`SaveAllAsync(...)` 的事件语义和逐项调用 `SaveAsync(...)` 不同。它代表一次显式的批量提交,因此适合让监听器在收到 `DataBatchSavedEvent` 时统一刷新 UI、缓存或元数据,而不是对每个条目单独响应。
|
||||
|
||||
### 聚合设置仓库
|
||||
|
||||
如果你希望把设置统一保存到单个文件中,可以使用 `UnifiedSettingsDataRepository`:
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Data;
|
||||
using GFramework.Game.Serializer;
|
||||
using GFramework.Game.Storage;
|
||||
|
||||
public sealed class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
var serializer = new JsonSerializer();
|
||||
var storage = new FileStorage("GameData", serializer, ".json");
|
||||
|
||||
ISettingsDataRepository settingsRepository = new UnifiedSettingsDataRepository(
|
||||
storage,
|
||||
serializer,
|
||||
new DataRepositoryOptions
|
||||
{
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
var serializer = new JsonSerializer();
|
||||
BasePath = "settings",
|
||||
AutoBackup = true
|
||||
});
|
||||
|
||||
var settingsRepo = new UnifiedSettingsDataRepository(
|
||||
storage,
|
||||
serializer,
|
||||
new DataRepositoryOptions
|
||||
{
|
||||
AutoBackup = true,
|
||||
EnableEvents = true
|
||||
},
|
||||
"settings.json");
|
||||
|
||||
settingsRepo.RegisterDataType(new DataLocation("settings", "graphics"), typeof(GraphicsSettings));
|
||||
settingsRepo.RegisterDataType(new DataLocation("settings", "audio"), typeof(AudioSettings));
|
||||
|
||||
RegisterUtility<ISettingsDataRepository>(settingsRepo);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个实现依然满足 `IDataRepository` 的通用契约,但有两个实现层面的差异需要明确:
|
||||
|
||||
- 它把所有设置 section 缓存在内存中,并在保存或删除时整文件回写
|
||||
- 开启自动备份时,备份的是整个 `settings.json` 文件,因此适合做“上一次完整设置快照”的恢复,而不是 section 级别回滚
|
||||
|
||||
如果你需要 `LoadAllAsync()`,或者希望在同一个统一文件里混合反序列化多个 section,必须先为每个 section 注册类型:
|
||||
|
||||
```csharp
|
||||
public async Task PrintSettingsSnapshot()
|
||||
var saveConfiguration = new SaveConfiguration
|
||||
{
|
||||
var repo = this.GetUtility<ISettingsDataRepository>();
|
||||
|
||||
var all = await repo.LoadAllAsync();
|
||||
|
||||
var graphics = (GraphicsSettings)all["graphics"];
|
||||
var audio = (AudioSettings)all["audio"];
|
||||
|
||||
Console.WriteLine($"Resolution: {graphics.ResolutionWidth}x{graphics.ResolutionHeight}");
|
||||
Console.WriteLine($"MasterVolume: {audio.MasterVolume}");
|
||||
}
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save.json"
|
||||
};
|
||||
```
|
||||
|
||||
最小采用要求:
|
||||
分工应保持清晰:
|
||||
|
||||
- 项目需要可用的 `IStorage`
|
||||
- 项目需要一个可用的序列化器实例,例如 `GFramework.Game.Serializer.JsonSerializer`
|
||||
- 在注册仓库时,把所有需要参与 `LoadAllAsync()` 或混合 section 反序列化的 location/type 对显式调用一次 `RegisterDataType(...)`
|
||||
- `storage` 只负责底层文件读写
|
||||
- `settingsRepository` 负责统一设置文件
|
||||
- `SaveRepository<TSaveData>` 负责槽位目录和存档迁移
|
||||
|
||||
兼容性说明:
|
||||
## 当前边界
|
||||
|
||||
- 现在 `UnifiedSettingsDataRepository.LoadAsync<T>()` 发送的是 `DataLoadedEvent<T>`,而不是 `DataLoadedEvent<IData>`
|
||||
- 如果你之前监听的是 `DataLoadedEvent<IData>`,需要改成订阅具体类型,例如 `DataLoadedEvent<GraphicsSettings>` 或 `DataLoadedEvent<AudioSettings>`
|
||||
- `DataRepositoryOptions` 描述的是仓库公开行为契约,不是某一种固定落盘格式
|
||||
- `UnifiedSettingsDataRepository` 不是通用万能仓库,它专门服务“多 section 聚合单文件”的场景
|
||||
- `SaveRepository<TSaveData>` 不负责业务层的 autosave 策略、云同步或存档选择 UI
|
||||
- `LoadAsync(...)` 返回新实例的行为适合默认启动路径;如果项目需要“缺档即报错”,应在业务层显式调用 `ExistsAsync(...)`
|
||||
|
||||
### 存档备份
|
||||
## 继续阅读
|
||||
|
||||
```csharp
|
||||
public async Task BackupSave(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
if (!await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
Console.WriteLine("存档不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载原存档
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
|
||||
// 保存到备份槽位
|
||||
int backupSlot = slot + 100;
|
||||
await saveRepo.SaveAsync(backupSlot, saveData);
|
||||
|
||||
Console.WriteLine($"存档已备份到槽位 {backupSlot}");
|
||||
}
|
||||
|
||||
public async Task RestoreBackup(int slot)
|
||||
{
|
||||
int backupSlot = slot + 100;
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
if (!await saveRepo.ExistsAsync(backupSlot))
|
||||
{
|
||||
Console.WriteLine("备份不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载备份
|
||||
var backupData = await saveRepo.LoadAsync(backupSlot);
|
||||
|
||||
// 恢复到原槽位
|
||||
await saveRepo.SaveAsync(slot, backupData);
|
||||
|
||||
Console.WriteLine($"已从备份恢复到槽位 {slot}");
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用版本化数据**:为存档数据实现 `IVersionedData`
|
||||
```csharp
|
||||
✓ public class SaveData : IVersionedData { public int Version { get; set; } = 1; }
|
||||
✗ public class SaveData : IData { } // 无法进行版本管理
|
||||
```
|
||||
|
||||
2. **定期自动保存**:避免玩家数据丢失
|
||||
```csharp
|
||||
// 每 5 分钟自动保存
|
||||
StartAutoSave(currentSlot, TimeSpan.FromMinutes(5));
|
||||
```
|
||||
|
||||
3. **保存前验证数据**:确保数据完整性
|
||||
```csharp
|
||||
public async Task SaveGame(int slot)
|
||||
{
|
||||
var saveData = CreateSaveData();
|
||||
|
||||
if (!ValidateSaveData(saveData))
|
||||
{
|
||||
throw new InvalidOperationException("存档数据无效");
|
||||
}
|
||||
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
}
|
||||
```
|
||||
|
||||
4. **处理保存失败**:使用 try-catch 捕获异常
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"保存失败: {ex.Message}");
|
||||
ShowErrorMessage("保存失败,请重试");
|
||||
}
|
||||
```
|
||||
|
||||
5. **提供多个存档槽位**:让玩家可以管理多个存档
|
||||
```csharp
|
||||
// 支持 10 个存档槽位
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
if (await saveRepo.ExistsAsync(i))
|
||||
{
|
||||
ShowSaveSlot(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **在关键时刻保存**:场景切换、关卡完成等
|
||||
```csharp
|
||||
public async Task OnLevelComplete()
|
||||
{
|
||||
// 关卡完成时自动保存
|
||||
await SaveGame(currentSlot);
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何实现多个存档槽位?
|
||||
|
||||
**解答**:
|
||||
使用 `ISaveRepository<T>` 的槽位参数:
|
||||
|
||||
```csharp
|
||||
// 保存到不同槽位
|
||||
await saveRepo.SaveAsync(1, saveData); // 槽位 1
|
||||
await saveRepo.SaveAsync(2, saveData); // 槽位 2
|
||||
await saveRepo.SaveAsync(3, saveData); // 槽位 3
|
||||
```
|
||||
|
||||
### 问题:如何处理数据版本升级?
|
||||
|
||||
**解答**:
|
||||
实现 `IVersionedData`,并在仓库初始化阶段注册 `ISaveMigration<TSaveData>`。之后 `LoadAsync(slot)` 会自动执行迁移并回写:
|
||||
|
||||
```csharp
|
||||
var saveRepo = new SaveRepository<SaveData>(storage, saveConfig)
|
||||
.RegisterMigration(new SaveDataMigrationV1ToV2())
|
||||
.RegisterMigration(new SaveDataMigrationV2ToV3());
|
||||
|
||||
var data = await saveRepo.LoadAsync(slot);
|
||||
```
|
||||
|
||||
### 问题:存档数据保存在哪里?
|
||||
|
||||
**解答**:
|
||||
由存储系统决定,通常在:
|
||||
|
||||
- Windows: `%AppData%/GameName/saves/`
|
||||
- Linux: `~/.local/share/GameName/saves/`
|
||||
- macOS: `~/Library/Application Support/GameName/saves/`
|
||||
|
||||
### 问题:如何实现云存档?
|
||||
|
||||
**解答**:
|
||||
实现自定义的 `IStorage`,将数据保存到云端:
|
||||
|
||||
```csharp
|
||||
public class CloudStorage : IStorage
|
||||
{
|
||||
public async Task WriteAsync(string path, byte[] data)
|
||||
{
|
||||
await UploadToCloud(path, data);
|
||||
}
|
||||
|
||||
public async Task<byte[]> ReadAsync(string path)
|
||||
{
|
||||
return await DownloadFromCloud(path);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何加密存档数据?
|
||||
|
||||
**解答**:
|
||||
在保存和加载时进行加密/解密:
|
||||
|
||||
```csharp
|
||||
public async Task SaveEncrypted(int slot, SaveData data)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
var encrypted = Encrypt(json);
|
||||
await storage.WriteAsync(path, encrypted);
|
||||
}
|
||||
|
||||
public async Task<SaveData> LoadEncrypted(int slot)
|
||||
{
|
||||
var encrypted = await storage.ReadAsync(path);
|
||||
var json = Decrypt(encrypted);
|
||||
return JsonSerializer.Deserialize<SaveData>(json);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:存档损坏怎么办?
|
||||
|
||||
**解答**:
|
||||
实现备份和恢复机制:
|
||||
|
||||
```csharp
|
||||
public async Task SaveWithBackup(int slot, SaveData data)
|
||||
{
|
||||
// 先备份旧存档
|
||||
if (await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
var oldData = await saveRepo.LoadAsync(slot);
|
||||
await saveRepo.SaveAsync(slot + 100, oldData);
|
||||
}
|
||||
|
||||
// 保存新存档
|
||||
await saveRepo.SaveAsync(slot, data);
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [设置系统](/zh-CN/game/setting) - 游戏设置管理
|
||||
- [场景系统](/zh-CN/game/scene) - 场景切换时保存
|
||||
- [存档系统实现教程](/zh-CN/tutorials/save-system) - 完整示例
|
||||
- [Godot 集成](/zh-CN/godot/index) - Godot 中的数据管理
|
||||
1. [设置系统](./setting.md)
|
||||
2. [存储系统](./storage.md)
|
||||
3. [序列化系统](./serialization.md)
|
||||
4. [Game 入口](./index.md)
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Game
|
||||
description: GFramework.Game family 的运行时入口、采用顺序与 XML 阅读基线。
|
||||
---
|
||||
|
||||
# Game
|
||||
|
||||
`Game` 栏目对应 `GFramework.Game` 与 `GFramework.Game.Abstractions` 这层游戏运行时能力。
|
||||
@ -98,6 +103,16 @@ IStorage storage = new FileStorage("GameData", serializer);
|
||||
4. [setting](./setting.md)
|
||||
5. [scene](./scene.md) 或 [ui](./ui.md)
|
||||
|
||||
## Game Family XML 覆盖基线
|
||||
|
||||
下面这份 inventory 记录的是 `2026-04-23` 对 `Game` family 做的一轮轻量 XML 盘点结果:只统计公开 / 内部类型声明是否带 XML 注释,用来建立 README / landing / API 阅读链路;成员级 `param`、`returns`、`exception` 与生命周期说明仍需要后续波次继续细化。
|
||||
|
||||
| 模块 | 基线状态 | 代表类型 | 阅读重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GFramework.Game` | `56/56` 个类型声明已带 XML 注释 | `YamlConfigLoader`、`SettingsModel<TRepository>`、`SceneRouterBase`、`UiRouterBase` | 先看运行时默认实现、配置加载、设置编排和路由基类 |
|
||||
| `GFramework.Game.Abstractions` | `80/80` 个类型声明已带 XML 注释 | `IConfigRegistry`、`ISaveRepository<TSaveData>`、`ISettingsSystem`、`ISceneRouter`、`IUiRouter` | 再看契约层边界,决定项目哪些程序集只依赖接口 |
|
||||
| `GFramework.Game.SourceGenerators` | `2/2` 个类型声明已带 XML 注释 | `SchemaConfigGenerator`、`ConfigSchemaDiagnostics` | 最后看 schema 生成入口与诊断模型,确认配置系统的编译期链路 |
|
||||
|
||||
## 与真实接法的关系
|
||||
|
||||
这个栏目以源码、`*.csproj`、模块 `README.md` 与 `ai-libs/` 下已验证的参考接法为准。
|
||||
|
||||
@ -1,811 +1,162 @@
|
||||
---
|
||||
title: 序列化系统
|
||||
description: 序列化系统提供了统一的对象序列化和反序列化接口,支持 JSON 格式和运行时类型处理。
|
||||
description: 以当前 GFramework.Game.JsonSerializer 与 JsonSerializerTests 为准,说明 JSON 序列化器的配置生命周期和使用边界。
|
||||
---
|
||||
|
||||
# 序列化系统
|
||||
|
||||
## 概述
|
||||
`GFramework.Game` 当前在序列化这一层的默认公开入口只有 `JsonSerializer`。
|
||||
|
||||
序列化系统是 GFramework.Game 中用于对象序列化和反序列化的核心组件。它提供了统一的序列化接口,支持将对象转换为字符串格式(如
|
||||
JSON)进行存储或传输,并能够将字符串数据还原为对象。
|
||||
它实现的是:
|
||||
|
||||
序列化系统与数据存储、配置管理、存档系统等模块深度集成,为游戏数据的持久化提供了基础支持。
|
||||
- `ISerializer`
|
||||
- `IRuntimeTypeSerializer`
|
||||
|
||||
**主要特性**:
|
||||
它不负责:
|
||||
|
||||
- 统一的序列化接口
|
||||
- JSON 格式支持
|
||||
- 运行时类型序列化
|
||||
- 泛型和非泛型 API
|
||||
- 与存储系统无缝集成
|
||||
- 类型安全的反序列化
|
||||
- schema 驱动配置生成
|
||||
- 存档槽位管理
|
||||
- 文件路径或目录布局
|
||||
|
||||
## 核心概念
|
||||
这些能力分别属于 source generator、repository 和 storage。
|
||||
|
||||
### 序列化器接口
|
||||
## 当前公开入口
|
||||
|
||||
`ISerializer` 定义了基本的序列化操作:
|
||||
### `JsonSerializer`
|
||||
|
||||
```csharp
|
||||
public interface ISerializer : IUtility
|
||||
{
|
||||
// 将对象序列化为字符串
|
||||
string Serialize<T>(T value);
|
||||
|
||||
// 将字符串反序列化为对象
|
||||
T Deserialize<T>(string data);
|
||||
}
|
||||
```
|
||||
|
||||
### 运行时类型序列化器
|
||||
|
||||
`IRuntimeTypeSerializer` 扩展了基本接口,支持运行时类型处理:
|
||||
|
||||
```csharp
|
||||
public interface IRuntimeTypeSerializer : ISerializer
|
||||
{
|
||||
// 使用运行时类型序列化对象
|
||||
string Serialize(object obj, Type type);
|
||||
|
||||
// 使用运行时类型反序列化对象
|
||||
object Deserialize(string data, Type type);
|
||||
}
|
||||
```
|
||||
|
||||
### JSON 序列化器
|
||||
|
||||
`JsonSerializer` 是基于 Newtonsoft.Json 的实现。它会直接复用构造时提供的
|
||||
`JsonSerializerSettings` 与 `Converters` 集合,因此推荐在组合根统一完成配置,再把同一个实例注册到架构或仓库中复用:
|
||||
|
||||
```csharp
|
||||
public sealed class JsonSerializer : IRuntimeTypeSerializer
|
||||
{
|
||||
string Serialize<T>(T value);
|
||||
T Deserialize<T>(string data);
|
||||
string Serialize(object obj, Type type);
|
||||
object Deserialize(string data, Type type);
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 注册序列化器
|
||||
|
||||
在架构中注册序列化器:
|
||||
`JsonSerializer` 基于 `Newtonsoft.Json`,既支持泛型 API,也支持运行时类型 API:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Serializer;
|
||||
using GFramework.Game.Serializer;
|
||||
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 在启动阶段一次性完成配置,后续将该实例视为只读
|
||||
var jsonSerializer = new JsonSerializer();
|
||||
jsonSerializer.Converters.Add(new PlayerDataJsonConverter());
|
||||
|
||||
RegisterUtility<ISerializer>(jsonSerializer);
|
||||
RegisterUtility<IRuntimeTypeSerializer>(jsonSerializer);
|
||||
}
|
||||
}
|
||||
ISerializer serializer = new JsonSerializer();
|
||||
IRuntimeTypeSerializer runtimeSerializer = new JsonSerializer();
|
||||
```
|
||||
|
||||
### 序列化对象
|
||||
当前测试覆盖的核心行为包括:
|
||||
|
||||
使用泛型 API 序列化对象:
|
||||
- 普通对象可正常 round-trip
|
||||
- 注入的 `JsonSerializerSettings` 会直接生效
|
||||
- `Settings` 与 `Converters` 暴露的是同一个活动配置实例
|
||||
- 运行时类型序列化 / 反序列化可处理 `object + Type`
|
||||
- 非法 JSON 会抛出带目标类型上下文的 `InvalidOperationException`
|
||||
- 非法参数(例如空字符串)会保留 `ArgumentException`
|
||||
- 运行时类型序列化允许 `null`,输出 `"null"`
|
||||
|
||||
## 配置生命周期
|
||||
|
||||
这部分是当前实现最容易被旧文档说错的地方。
|
||||
|
||||
`JsonSerializer` 不会复制你传入的 `JsonSerializerSettings`。它会直接持有并复用这份实例,以及里面的 `Converters` 集合。
|
||||
|
||||
这意味着推荐模式是:
|
||||
|
||||
1. 在组合根创建序列化器
|
||||
2. 一次性完成 settings / converters 配置
|
||||
3. 再把同一个实例注册给存储、repository 或 architecture
|
||||
|
||||
推荐写法:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
public class PlayerData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; }
|
||||
}
|
||||
|
||||
[ContextAware]
|
||||
public partial class SaveController : IController
|
||||
{
|
||||
public void SavePlayer()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
var player = new PlayerData
|
||||
{
|
||||
Name = "Player1",
|
||||
Level = 10,
|
||||
Experience = 1000
|
||||
};
|
||||
|
||||
// 序列化为 JSON 字符串
|
||||
string json = serializer.Serialize(player);
|
||||
Console.WriteLine(json);
|
||||
// 输出: {"Name":"Player1","Level":10,"Experience":1000}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 反序列化对象
|
||||
|
||||
从字符串还原对象:
|
||||
|
||||
```csharp
|
||||
public void LoadPlayer()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
string json = "{\"Name\":\"Player1\",\"Level\":10,\"Experience\":1000}";
|
||||
|
||||
// 反序列化为对象
|
||||
var player = serializer.Deserialize<PlayerData>(json);
|
||||
|
||||
Console.WriteLine($"玩家: {player.Name}, 等级: {player.Level}");
|
||||
}
|
||||
```
|
||||
|
||||
### 运行时类型序列化
|
||||
|
||||
处理不确定类型的对象:
|
||||
|
||||
```csharp
|
||||
public void SerializeRuntimeType()
|
||||
{
|
||||
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
|
||||
|
||||
object data = new PlayerData { Name = "Player1", Level = 10 };
|
||||
Type dataType = data.GetType();
|
||||
|
||||
// 使用运行时类型序列化
|
||||
string json = serializer.Serialize(data, dataType);
|
||||
|
||||
// 使用运行时类型反序列化
|
||||
object restored = serializer.Deserialize(json, dataType);
|
||||
|
||||
var player = restored as PlayerData;
|
||||
Console.WriteLine($"玩家: {player?.Name}");
|
||||
}
|
||||
```
|
||||
|
||||
### 配置生命周期约束
|
||||
|
||||
`JsonSerializer` 不会复制 `JsonSerializerSettings`。这意味着:
|
||||
|
||||
- 传给构造函数的 settings 会被原样保留
|
||||
- `serializer.Settings` 与 `serializer.Converters` 返回的都是活动配置对象
|
||||
- 一旦序列化器实例已经注册给其他模块或开始被并发调用,就不应继续修改这些配置
|
||||
|
||||
推荐模式:
|
||||
|
||||
```csharp
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
settings.Converters.Add(new Vector2JsonConverter());
|
||||
settings.Converters.Add(new CoordinateConverter());
|
||||
|
||||
var serializer = new JsonSerializer(settings);
|
||||
```
|
||||
|
||||
不推荐写法:
|
||||
|
||||
```csharp
|
||||
var serializer = architecture.GetUtility<IRuntimeTypeSerializer>();
|
||||
|
||||
// 序列化器已经被多个组件共享后,再继续改 converter,容易让并发调用看到不稳定配置。
|
||||
((JsonSerializer)serializer).Converters.Add(new LateBoundConverter());
|
||||
```
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
### 作为底层 serializer 注册
|
||||
|
||||
当前更常见的采用方式不是“业务代码直接到处调 serializer”,而是把它注册给存储和 repository 复用:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Serializer;
|
||||
using GFramework.Game.Serializer;
|
||||
|
||||
var serializer = new JsonSerializer();
|
||||
|
||||
architecture.RegisterUtility<ISerializer>(serializer);
|
||||
architecture.RegisterUtility<IRuntimeTypeSerializer>(serializer);
|
||||
```
|
||||
|
||||
不推荐模式:
|
||||
然后由:
|
||||
|
||||
- `FileStorage`
|
||||
- `UnifiedSettingsDataRepository`
|
||||
- 其他依赖 `ISerializer` / `IRuntimeTypeSerializer` 的组件
|
||||
|
||||
统一复用这一份实例。
|
||||
|
||||
### 直接处理运行时类型
|
||||
|
||||
当业务层拿到的是 `object + Type` 组合,而不是静态泛型类型时,再使用运行时 API:
|
||||
|
||||
```csharp
|
||||
var serializer = architecture.GetUtility<IRuntimeTypeSerializer>();
|
||||
var serializer = new JsonSerializer();
|
||||
|
||||
// 已经共享给运行时后再修改配置,容易让并发调用得到不稳定行为
|
||||
((JsonSerializer)serializer).Converters.Add(new LateBoundConverter());
|
||||
object data = new PlayerState
|
||||
{
|
||||
Name = "Runtime",
|
||||
Level = 11
|
||||
};
|
||||
|
||||
var json = serializer.Serialize(data, data.GetType());
|
||||
var restored = serializer.Deserialize(json, data.GetType());
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
## 与存储系统的关系
|
||||
|
||||
### 与存储系统集成
|
||||
`FileStorage` 已经会调用注入的 `ISerializer` 自己完成对象读写,因此当前默认接法里:
|
||||
|
||||
序列化器与存储系统配合使用:
|
||||
- 你可以直接 `storage.WriteAsync("profile/player", profile)`
|
||||
- 不需要先手工 `serializer.Serialize(profile)` 再把字符串写回存储
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Storage;
|
||||
using GFramework.Game.Storage;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
手工显式调用 `Serialize(...)` 更适合这些场景:
|
||||
|
||||
[ContextAware]
|
||||
public partial class DataManager : IController
|
||||
{
|
||||
public async Task SaveData()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
- 需要把 JSON 发到网络或日志
|
||||
- 需要和外部文本格式做中转
|
||||
- 需要直接调试序列化输出内容
|
||||
|
||||
var gameData = new GameData
|
||||
{
|
||||
Score = 1000,
|
||||
Coins = 500
|
||||
};
|
||||
如果目标只是本地持久化,优先让 `IStorage` / repository 复用 serializer。
|
||||
|
||||
// 序列化数据
|
||||
string json = serializer.Serialize(gameData);
|
||||
## 与配置系统的关系
|
||||
|
||||
// 写入存储
|
||||
await storage.WriteAsync("game_data", json);
|
||||
}
|
||||
不要把 `JsonSerializer` 和 `Game` 的 YAML 配置系统混在一起:
|
||||
|
||||
public async Task<GameData> LoadData()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
- `JsonSerializer`
|
||||
- 负责运行时对象 JSON 序列化
|
||||
- `Game.SourceGenerators + YamlConfigLoader`
|
||||
- 负责 schema 驱动的配置表生成与 YAML 读取
|
||||
|
||||
// 从存储读取
|
||||
string json = await storage.ReadAsync<string>("game_data");
|
||||
如果你的目标是静态内容配置表,而不是运行时持久化对象,请改看 [`config-system.md`](./config-system.md)。
|
||||
|
||||
// 反序列化数据
|
||||
return serializer.Deserialize<GameData>(json);
|
||||
}
|
||||
}
|
||||
```
|
||||
## 当前边界
|
||||
|
||||
### 序列化复杂对象
|
||||
- 当前公开默认实现只有 JSON,没有内建 MessagePack、Binary 或 ProtoBuf 实现
|
||||
- `JsonSerializer` 负责序列化,不负责对象版本迁移;版本迁移属于 `SettingsModel<TRepository>` 或 `SaveRepository<TSaveData>`
|
||||
- 序列化器共享后应视为只读配置对象,避免在运行期继续修改 settings / converters
|
||||
|
||||
处理嵌套和集合类型:
|
||||
## 继续阅读
|
||||
|
||||
```csharp
|
||||
public class InventoryData
|
||||
{
|
||||
public List<ItemData> Items { get; set; }
|
||||
public Dictionary<string, int> Resources { get; set; }
|
||||
}
|
||||
|
||||
public class ItemData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
}
|
||||
|
||||
public void SerializeComplexData()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
var inventory = new InventoryData
|
||||
{
|
||||
Items = new List<ItemData>
|
||||
{
|
||||
new ItemData { Id = "sword_01", Name = "铁剑", Quantity = 1 },
|
||||
new ItemData { Id = "potion_hp", Name = "生命药水", Quantity = 5 }
|
||||
},
|
||||
Resources = new Dictionary<string, int>
|
||||
{
|
||||
{ "gold", 1000 },
|
||||
{ "wood", 500 }
|
||||
}
|
||||
};
|
||||
|
||||
// 序列化复杂对象
|
||||
string json = serializer.Serialize(inventory);
|
||||
|
||||
// 反序列化
|
||||
var restored = serializer.Deserialize<InventoryData>(json);
|
||||
|
||||
Console.WriteLine($"物品数量: {restored.Items.Count}");
|
||||
Console.WriteLine($"金币: {restored.Resources["gold"]}");
|
||||
}
|
||||
```
|
||||
|
||||
### 处理多态类型
|
||||
|
||||
序列化继承层次结构:
|
||||
|
||||
```csharp
|
||||
public abstract class EntityData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerEntityData : EntityData
|
||||
{
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; }
|
||||
}
|
||||
|
||||
public class EnemyEntityData : EntityData
|
||||
{
|
||||
public int Health { get; set; }
|
||||
public int Damage { get; set; }
|
||||
}
|
||||
|
||||
public void SerializePolymorphic()
|
||||
{
|
||||
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
|
||||
|
||||
// 创建不同类型的实体
|
||||
EntityData player = new PlayerEntityData
|
||||
{
|
||||
Id = "player_1",
|
||||
Type = "Player",
|
||||
Level = 10,
|
||||
Experience = 1000
|
||||
};
|
||||
|
||||
EntityData enemy = new EnemyEntityData
|
||||
{
|
||||
Id = "enemy_1",
|
||||
Type = "Enemy",
|
||||
Health = 100,
|
||||
Damage = 20
|
||||
};
|
||||
|
||||
// 使用运行时类型序列化
|
||||
string playerJson = serializer.Serialize(player, player.GetType());
|
||||
string enemyJson = serializer.Serialize(enemy, enemy.GetType());
|
||||
|
||||
// 根据类型反序列化
|
||||
var restoredPlayer = serializer.Deserialize(playerJson, typeof(PlayerEntityData));
|
||||
var restoredEnemy = serializer.Deserialize(enemyJson, typeof(EnemyEntityData));
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义序列化逻辑
|
||||
|
||||
虽然 GFramework 使用 Newtonsoft.Json,但你可以通过特性控制序列化行为:
|
||||
|
||||
```csharp
|
||||
using Newtonsoft.Json;
|
||||
|
||||
public class CustomData
|
||||
{
|
||||
// 忽略此属性
|
||||
[JsonIgnore]
|
||||
public string InternalId { get; set; }
|
||||
|
||||
// 使用不同的属性名
|
||||
[JsonProperty("player_name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
// 仅在值不为 null 时序列化
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string? OptionalField { get; set; }
|
||||
|
||||
// 格式化日期
|
||||
[JsonProperty("created_at")]
|
||||
[JsonConverter(typeof(IsoDateTimeConverter))]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 批量序列化
|
||||
|
||||
处理多个对象的序列化:
|
||||
|
||||
```csharp
|
||||
public async Task SaveMultipleData()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
var dataList = new Dictionary<string, object>
|
||||
{
|
||||
{ "player", new PlayerData { Name = "Player1", Level = 10 } },
|
||||
{ "inventory", new InventoryData { Items = new List<ItemData>() } },
|
||||
{ "settings", new SettingsData { Volume = 0.8f } }
|
||||
};
|
||||
|
||||
// 批量序列化和保存
|
||||
foreach (var (key, data) in dataList)
|
||||
{
|
||||
string json = serializer.Serialize(data);
|
||||
await storage.WriteAsync(key, json);
|
||||
}
|
||||
|
||||
Console.WriteLine($"已保存 {dataList.Count} 个数据文件");
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
处理序列化和反序列化错误:
|
||||
|
||||
```csharp
|
||||
public void SafeDeserialize()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
string json = "{\"Name\":\"Player1\",\"Level\":\"invalid\"}"; // 错误的数据
|
||||
|
||||
try
|
||||
{
|
||||
var player = serializer.Deserialize<PlayerData>(json);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Console.WriteLine($"反序列化失败: {ex.Message}");
|
||||
// 返回默认值或重新尝试
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Console.WriteLine($"JSON 格式错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerData DeserializeWithFallback(string json)
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
try
|
||||
{
|
||||
return serializer.Deserialize<PlayerData>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 返回默认数据
|
||||
return new PlayerData
|
||||
{
|
||||
Name = "DefaultPlayer",
|
||||
Level = 1,
|
||||
Experience = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 版本兼容性
|
||||
|
||||
处理数据结构变化:
|
||||
|
||||
```csharp
|
||||
// 旧版本数据
|
||||
public class PlayerDataV1
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
// 新版本数据(添加了新字段)
|
||||
public class PlayerDataV2
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; } = 0; // 新增字段,提供默认值
|
||||
public DateTime LastLogin { get; set; } = DateTime.Now; // 新增字段
|
||||
}
|
||||
|
||||
public PlayerDataV2 LoadWithMigration(string json)
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试加载新版本
|
||||
return serializer.Deserialize<PlayerDataV2>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果失败,尝试加载旧版本并迁移
|
||||
var oldData = serializer.Deserialize<PlayerDataV1>(json);
|
||||
return new PlayerDataV2
|
||||
{
|
||||
Name = oldData.Name,
|
||||
Level = oldData.Level,
|
||||
Experience = oldData.Level * 100, // 根据等级计算经验
|
||||
LastLogin = DateTime.Now
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **优先依赖接口,在组合根统一实例化**:业务代码依赖 `ISerializer`,具体 `JsonSerializer` 在启动阶段配置一次即可
|
||||
```csharp
|
||||
✓ var serializer = this.GetUtility<ISerializer>();
|
||||
✓ var serializer = new JsonSerializer(settings); // 仅在组合根/启动阶段配置
|
||||
✗ var serializer = new JsonSerializer(); // 避免在业务逻辑中临时创建
|
||||
```
|
||||
|
||||
2. **为数据类提供默认值**:确保反序列化的健壮性
|
||||
```csharp
|
||||
public class GameData
|
||||
{
|
||||
public string Name { get; set; } = "Default";
|
||||
public int Score { get; set; } = 0;
|
||||
public List<string> Items { get; set; } = new();
|
||||
}
|
||||
```
|
||||
|
||||
3. **处理反序列化异常**:避免程序崩溃
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
var data = serializer.Deserialize<GameData>(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"反序列化失败: {ex.Message}");
|
||||
return GetDefaultData();
|
||||
}
|
||||
```
|
||||
|
||||
4. **避免序列化敏感数据**:使用 `[JsonIgnore]` 标记
|
||||
```csharp
|
||||
public class UserData
|
||||
{
|
||||
public string Username { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Password { get; set; } // 不序列化密码
|
||||
}
|
||||
```
|
||||
|
||||
5. **使用运行时类型处理多态**:保持类型信息
|
||||
```csharp
|
||||
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
|
||||
string json = serializer.Serialize(obj, obj.GetType());
|
||||
```
|
||||
|
||||
6. **验证反序列化的数据**:确保数据完整性
|
||||
```csharp
|
||||
var data = serializer.Deserialize<GameData>(json);
|
||||
if (string.IsNullOrEmpty(data.Name) || data.Score < 0)
|
||||
{
|
||||
throw new InvalidDataException("数据验证失败");
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 减少序列化开销
|
||||
|
||||
```csharp
|
||||
// 避免频繁序列化大对象
|
||||
public class CachedSerializer
|
||||
{
|
||||
private string? _cachedJson;
|
||||
private GameData? _cachedData;
|
||||
|
||||
public string GetJson(GameData data)
|
||||
{
|
||||
if (_cachedData == data && _cachedJson != null)
|
||||
{
|
||||
return _cachedJson;
|
||||
}
|
||||
|
||||
var serializer = GetSerializer();
|
||||
_cachedJson = serializer.Serialize(data);
|
||||
_cachedData = data;
|
||||
return _cachedJson;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步序列化
|
||||
|
||||
```csharp
|
||||
public async Task SaveDataAsync()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
var data = GetLargeData();
|
||||
|
||||
// 在后台线程序列化
|
||||
string json = await Task.Run(() => serializer.Serialize(data));
|
||||
|
||||
// 异步写入存储
|
||||
await storage.WriteAsync("large_data", json);
|
||||
}
|
||||
```
|
||||
|
||||
### 分块序列化
|
||||
|
||||
```csharp
|
||||
public async Task SaveLargeDataset()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
var largeDataset = GetLargeDataset();
|
||||
|
||||
// 分块保存
|
||||
const int chunkSize = 100;
|
||||
for (int i = 0; i < largeDataset.Count; i += chunkSize)
|
||||
{
|
||||
var chunk = largeDataset.Skip(i).Take(chunkSize).ToList();
|
||||
string json = serializer.Serialize(chunk);
|
||||
await storage.WriteAsync($"data_chunk_{i / chunkSize}", json);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何序列化循环引用的对象?
|
||||
|
||||
**解答**:
|
||||
Newtonsoft.Json 默认不支持循环引用,需要配置:
|
||||
|
||||
```csharp
|
||||
// 注意:GFramework 的 JsonSerializer 使用默认设置
|
||||
// 如需处理循环引用,避免创建循环引用的数据结构
|
||||
// 或使用 [JsonIgnore] 打破循环
|
||||
|
||||
public class Node
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public List<Node> Children { get; set; }
|
||||
|
||||
[JsonIgnore] // 忽略父节点引用,避免循环
|
||||
public Node? Parent { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:序列化后的 JSON 太大怎么办?
|
||||
|
||||
**解答**:
|
||||
使用压缩或分块存储:
|
||||
|
||||
```csharp
|
||||
public async Task SaveCompressed()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
var data = GetLargeData();
|
||||
string json = serializer.Serialize(data);
|
||||
|
||||
// 压缩 JSON
|
||||
byte[] compressed = Compress(json);
|
||||
|
||||
// 保存压缩数据
|
||||
await storage.WriteAsync("data_compressed", compressed);
|
||||
}
|
||||
|
||||
private byte[] Compress(string text)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
using (var gzip = new GZipStream(output, CompressionMode.Compress))
|
||||
using (var writer = new StreamWriter(gzip))
|
||||
{
|
||||
writer.Write(text);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何处理不同平台的序列化差异?
|
||||
|
||||
**解答**:
|
||||
使用平台无关的数据类型:
|
||||
|
||||
```csharp
|
||||
public class CrossPlatformData
|
||||
{
|
||||
// 使用 string 而非 DateTime(避免时区问题)
|
||||
public string CreatedAt { get; set; } = DateTime.UtcNow.ToString("O");
|
||||
|
||||
// 使用 double 而非 float(精度一致)
|
||||
public double Score { get; set; }
|
||||
|
||||
// 明确指定编码
|
||||
public string Text { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:反序列化失败时如何恢复?
|
||||
|
||||
**解答**:
|
||||
实现备份和恢复机制:
|
||||
|
||||
```csharp
|
||||
public async Task<GameData> LoadWithBackup(string key)
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试加载主数据
|
||||
string json = await storage.ReadAsync<string>(key);
|
||||
return serializer.Deserialize<GameData>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 尝试加载备份
|
||||
try
|
||||
{
|
||||
string backupJson = await storage.ReadAsync<string>($"{key}_backup");
|
||||
return serializer.Deserialize<GameData>(backupJson);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 返回默认数据
|
||||
return new GameData();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何加密序列化的数据?
|
||||
|
||||
**解答**:
|
||||
在序列化后加密:
|
||||
|
||||
```csharp
|
||||
public async Task SaveEncrypted(string key, GameData data)
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
// 序列化
|
||||
string json = serializer.Serialize(data);
|
||||
|
||||
// 加密
|
||||
byte[] encrypted = EncryptString(json);
|
||||
|
||||
// 保存
|
||||
await storage.WriteAsync(key, encrypted);
|
||||
}
|
||||
|
||||
public async Task<GameData> LoadEncrypted(string key)
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
// 读取
|
||||
byte[] encrypted = await storage.ReadAsync<byte[]>(key);
|
||||
|
||||
// 解密
|
||||
string json = DecryptToString(encrypted);
|
||||
|
||||
// 反序列化
|
||||
return serializer.Deserialize<GameData>(json);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:序列化器是线程安全的吗?
|
||||
|
||||
**解答**:
|
||||
`JsonSerializer` 的并发读行为只在“配置不再变化”这个前提下才安全。当前实现会暴露活动中的
|
||||
`JsonSerializerSettings` 与 `Converters` 集合,因此:
|
||||
|
||||
- 可以在启动阶段创建并配置一个共享实例
|
||||
- 可以在配置冻结后把该实例注册为 Utility 或注入到仓库
|
||||
- 不应在序列化器已经被多个调用方使用时继续修改 settings、contract resolver 或 converters
|
||||
|
||||
推荐按下面的方式在启动阶段完成配置,然后只做读操作:
|
||||
|
||||
```csharp
|
||||
// 启动阶段完成全部配置
|
||||
var serializer = new JsonSerializer(new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
serializer.Converters.Add(new GameDataJsonConverter());
|
||||
|
||||
architecture.RegisterUtility<ISerializer>(serializer);
|
||||
|
||||
// 运行阶段只复用,不再修改配置
|
||||
public async Task ParallelSave()
|
||||
{
|
||||
var tasks = Enumerable.Range(0, 10).Select(async i =>
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var data = new GameData { Score = i };
|
||||
string json = serializer.Serialize(data);
|
||||
await SaveToStorage($"data_{i}", json);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
|
||||
- [存储系统](/zh-CN/game/storage) - 文件存储
|
||||
- [设置系统](/zh-CN/game/setting) - 设置数据序列化
|
||||
- [Utility 系统](/zh-CN/core/utility) - 工具类注册
|
||||
1. [存储系统](./storage.md)
|
||||
2. [数据与存档系统](./data.md)
|
||||
3. [配置系统](./config-system.md)
|
||||
4. [Game 入口](./index.md)
|
||||
|
||||
@ -1,207 +1,204 @@
|
||||
---
|
||||
title: 设置系统
|
||||
description: 以当前 SettingsModel、SettingsSystem 与相关测试为准,说明设置数据、applicator、迁移和持久化的真实接法。
|
||||
---
|
||||
|
||||
# 设置系统
|
||||
|
||||
设置系统负责管理 `ISettingsData`、持久化加载/保存,以及把设置真正应用到运行时环境。
|
||||
`GFramework.Game` 的设置系统负责三件事:
|
||||
|
||||
当前实现以 `SettingsModel<TRepository>` 和 `SettingsSystem` 为核心,已经不是旧文档中的
|
||||
`Get<T>() / Register(IApplyAbleSettings)` 接口模型。
|
||||
- 管理 `ISettingsData` 实例的生命周期
|
||||
- 管理设置 applicator,并把设置真正作用到运行时环境
|
||||
- 在初始化时加载、迁移、保存和重置设置
|
||||
|
||||
## 核心概念
|
||||
当前默认 owner 是:
|
||||
|
||||
### ISettingsData
|
||||
- `SettingsModel<TRepository>`
|
||||
- `SettingsSystem`
|
||||
|
||||
设置数据对象负责保存设置值、提供默认值,并在加载后把外部数据回填到当前实例。
|
||||
而不是旧文档里那种“只靠若干 `Get<T>() / Register(...)` 辅助方法就能自动完成一切”的模型。
|
||||
|
||||
## 当前公开入口
|
||||
|
||||
### `ISettingsData`
|
||||
|
||||
设置数据对象需要同时承担:
|
||||
|
||||
- 默认值持有者
|
||||
- 版本化 section
|
||||
- 从已加载数据回填到当前实例的入口
|
||||
|
||||
当前接口组合是:
|
||||
|
||||
```csharp
|
||||
public interface ISettingsData : IResettable, IVersionedData, ILoadableFrom<ISettingsData>;
|
||||
```
|
||||
|
||||
这意味着一个设置数据类型通常需要实现:
|
||||
这意味着一个设置数据类型至少要处理:
|
||||
|
||||
- `Reset()`:恢复默认值
|
||||
- `Version` / `LastModified`:暴露版本化信息
|
||||
- `LoadFrom(ISettingsData)`:把已加载或迁移后的数据复制到当前实例
|
||||
- `Reset()`
|
||||
- `Version`
|
||||
- `LastModified`
|
||||
- `LoadFrom(ISettingsData source)`
|
||||
|
||||
### IResetApplyAbleSettings
|
||||
### `IResetApplyAbleSettings`
|
||||
|
||||
应用器负责把设置数据作用到引擎或运行时环境:
|
||||
applicator 的职责不是保存数据,而是把设置结果作用到实际运行时对象。
|
||||
|
||||
它当前需要暴露:
|
||||
|
||||
- `Data`
|
||||
- `DataType`
|
||||
- `Reset()`
|
||||
- `ApplyAsync()`
|
||||
|
||||
典型场景包括:
|
||||
|
||||
- 把音量设置同步到音频系统
|
||||
- 把画质设置同步到窗口或渲染配置
|
||||
- 把语言设置同步到本地化服务
|
||||
|
||||
### `SettingsModel<TRepository>`
|
||||
|
||||
这是当前设置系统的核心编排器。按当前源码,它负责:
|
||||
|
||||
- `GetData<T>()`
|
||||
- 返回某个设置类型的唯一实例
|
||||
- `RegisterApplicator(...)`
|
||||
- 注册 applicator,并把其 `Data` 一并纳入模型管理
|
||||
- `RegisterMigration(...)`
|
||||
- 注册同一设置类型的前进式迁移链
|
||||
- `InitializeAsync()`
|
||||
- 从 repository 读取所有设置、执行迁移、回填到当前实例
|
||||
- `SaveAllAsync()`
|
||||
- 持久化所有已登记的设置数据
|
||||
- `ApplyAllAsync()`
|
||||
- 依次应用所有 applicator
|
||||
- `Reset<T>() / ResetAll()`
|
||||
- 重置单个或全部设置
|
||||
|
||||
### `SettingsSystem`
|
||||
|
||||
`SettingsSystem` 是面向业务代码更直接的一层系统封装:
|
||||
|
||||
- `ApplyAll()`
|
||||
- `Apply<T>()`
|
||||
- `SaveAll()`
|
||||
- `Reset<T>()`
|
||||
- `ResetAll()`
|
||||
|
||||
它自己不持有独立设置状态,而是把工作委托给 `ISettingsModel`,并在应用时补发 settings 相关事件。
|
||||
|
||||
## 初始化与迁移的真实语义
|
||||
|
||||
`SettingsModel<TRepository>.InitializeAsync()` 的当前行为,比旧文档里“加载一下就好”更严格一些:
|
||||
|
||||
- 它会先调用 `ISettingsDataRepository.LoadAllAsync()`
|
||||
- 再逐个匹配当前模型里已经登记的设置类型
|
||||
- 如果读到了旧版本设置,会以“当前内存实例声明的 `Version`”为目标版本执行迁移
|
||||
- 迁移完成后通过 `LoadFrom(...)` 回填到现有实例,而不是直接替换对象引用
|
||||
|
||||
当前测试还确认了几个关键边界:
|
||||
|
||||
- 同一设置类型的同一个 `FromVersion` 不能重复注册迁移器
|
||||
- 注册新迁移器后,类型级迁移缓存会失效并重建,不会继续使用旧快照
|
||||
- 如果迁移链缺口导致无法安全升级,模型会保留当前内存中的最新实例,而不是把不完整的旧数据覆盖进来
|
||||
- 单个设置 section 初始化失败时,模型会记录错误并继续处理其他 section
|
||||
|
||||
这套语义更接近“尽量保证运行时实例总是可用”,而不是“任意旧设置都必须成功导入”。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
当前最常见的接法是:
|
||||
|
||||
1. 准备一个 `IStorage`
|
||||
2. 准备一个 `IRuntimeTypeSerializer`
|
||||
3. 注册 `ISettingsDataRepository`
|
||||
4. 注册 `IDataLocationProvider`
|
||||
5. 创建并注册 `SettingsModel<TRepository>`
|
||||
6. 注册 applicator
|
||||
7. 注册 `SettingsSystem`
|
||||
|
||||
示意代码:
|
||||
|
||||
```csharp
|
||||
public interface IResetApplyAbleSettings : IResettable, IApplyAbleSettings
|
||||
{
|
||||
ISettingsData Data { get; }
|
||||
Type DataType { get; }
|
||||
}
|
||||
using GFramework.Core.Abstractions.Storage;
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Abstractions.Setting;
|
||||
using GFramework.Game.Data;
|
||||
using GFramework.Game.Serializer;
|
||||
using GFramework.Game.Setting;
|
||||
using GFramework.Game.Storage;
|
||||
|
||||
var serializer = new JsonSerializer();
|
||||
var storage = new FileStorage("GameData", serializer, ".json");
|
||||
|
||||
var repository = new UnifiedSettingsDataRepository(
|
||||
storage,
|
||||
serializer,
|
||||
new DataRepositoryOptions
|
||||
{
|
||||
BasePath = "settings",
|
||||
AutoBackup = true
|
||||
});
|
||||
|
||||
architecture.RegisterUtility<IStorage>(storage);
|
||||
architecture.RegisterUtility<IRuntimeTypeSerializer>(serializer);
|
||||
architecture.RegisterUtility<ISettingsDataRepository>(repository);
|
||||
// 此处注册项目侧的 IDataLocationProvider 实现,用于把设置类型映射到 section key。
|
||||
|
||||
var settingsModel = new SettingsModel<ISettingsDataRepository>(null, null);
|
||||
// 在注册到架构前,继续补 applicator 与 migration。
|
||||
|
||||
architecture.RegisterModel<ISettingsModel>(settingsModel);
|
||||
architecture.RegisterSystem<ISettingsSystem>(new SettingsSystem());
|
||||
```
|
||||
|
||||
常见用途包括:
|
||||
|
||||
- 把音量设置同步到音频总线
|
||||
- 把图形设置同步到窗口系统
|
||||
- 把语言设置同步到本地化管理器
|
||||
|
||||
## ISettingsModel
|
||||
|
||||
当前 `ISettingsModel` 的主要 API 如下:
|
||||
启动阶段通常是:
|
||||
|
||||
```csharp
|
||||
public interface ISettingsModel : IModel
|
||||
{
|
||||
bool IsInitialized { get; }
|
||||
|
||||
T GetData<T>() where T : class, ISettingsData, new();
|
||||
IEnumerable<ISettingsData> AllData();
|
||||
|
||||
ISettingsModel RegisterApplicator<T>(T applicator)
|
||||
where T : class, IResetApplyAbleSettings;
|
||||
T? GetApplicator<T>() where T : class, IResetApplyAbleSettings;
|
||||
IEnumerable<IResetApplyAbleSettings> AllApplicators();
|
||||
|
||||
ISettingsModel RegisterMigration(ISettingsMigration migration);
|
||||
|
||||
Task InitializeAsync();
|
||||
Task SaveAllAsync();
|
||||
Task ApplyAllAsync();
|
||||
void Reset<T>() where T : class, ISettingsData, new();
|
||||
void ResetAll();
|
||||
}
|
||||
```
|
||||
|
||||
行为说明:
|
||||
|
||||
- `GetData<T>()` 返回某个设置数据的唯一实例
|
||||
- `RegisterApplicator<T>()` 注册应用器,并把其 `Data` 纳入模型管理
|
||||
- `InitializeAsync()` 从 `ISettingsDataRepository` 读取所有已注册设置,并在需要时执行迁移
|
||||
- `SaveAllAsync()` 持久化当前所有设置数据
|
||||
- `ApplyAllAsync()` 依次调用所有 applicator 的 `ApplyAsync()`
|
||||
|
||||
## SettingsSystem
|
||||
|
||||
`SettingsSystem` 是对模型的系统级封装,面向业务代码提供更直接的入口:
|
||||
|
||||
```csharp
|
||||
public interface ISettingsSystem : ISystem
|
||||
{
|
||||
Task ApplyAll();
|
||||
Task Apply<T>() where T : class, IResetApplyAbleSettings;
|
||||
Task SaveAll();
|
||||
Task Reset<T>() where T : class, ISettingsData, IResetApplyAbleSettings, new();
|
||||
Task ResetAll();
|
||||
}
|
||||
```
|
||||
|
||||
它不会自己保存数据,而是把保存、重置和应用逻辑委托给 `ISettingsModel`。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义设置数据
|
||||
|
||||
```csharp
|
||||
public sealed class GameplaySettings : ISettingsData
|
||||
{
|
||||
public float GameSpeed { get; set; } = 1.0f;
|
||||
|
||||
public int Version { get; private set; } = 1;
|
||||
public DateTime LastModified { get; } = DateTime.UtcNow;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
GameSpeed = 1.0f;
|
||||
}
|
||||
|
||||
public void LoadFrom(ISettingsData source)
|
||||
{
|
||||
if (source is not GameplaySettings settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameSpeed = settings.GameSpeed;
|
||||
Version = settings.Version;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 定义 applicator
|
||||
|
||||
```csharp
|
||||
public sealed class GameplaySettingsApplicator : IResetApplyAbleSettings
|
||||
{
|
||||
public GameplaySettingsApplicator(GameplaySettings data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public ISettingsData Data { get; }
|
||||
public Type DataType => typeof(GameplaySettings);
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Data.Reset();
|
||||
}
|
||||
|
||||
public Task ApplyAsync()
|
||||
{
|
||||
var settings = (GameplaySettings)Data;
|
||||
TimeScale.Current = settings.GameSpeed;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用模型和系统
|
||||
|
||||
```csharp
|
||||
var settingsModel = this.GetModel<ISettingsModel>();
|
||||
|
||||
var gameplayData = settingsModel.GetData<GameplaySettings>();
|
||||
gameplayData.GameSpeed = 1.25f;
|
||||
|
||||
settingsModel.RegisterApplicator(new GameplaySettingsApplicator(gameplayData));
|
||||
|
||||
await settingsModel.InitializeAsync();
|
||||
await settingsModel.SaveAllAsync();
|
||||
|
||||
var settingsSystem = this.GetSystem<ISettingsSystem>();
|
||||
await settingsSystem.ApplyAll();
|
||||
await settingsModel.ApplyAllAsync();
|
||||
```
|
||||
|
||||
## 迁移
|
||||
|
||||
设置系统内建了迁移注册入口:
|
||||
退出或显式保存时:
|
||||
|
||||
```csharp
|
||||
public interface ISettingsMigration
|
||||
{
|
||||
Type SettingsType { get; }
|
||||
int FromVersion { get; }
|
||||
int ToVersion { get; }
|
||||
ISettingsData Migrate(ISettingsData oldData);
|
||||
}
|
||||
await settingsModel.SaveAllAsync();
|
||||
```
|
||||
|
||||
当 `InitializeAsync()` 读取到旧版本设置时,会按已注册迁移链逐步升级,再通过 `LoadFrom` 回填到当前实例。
|
||||
## `GetData<T>()` 和 `RegisterApplicator(...)` 的分工
|
||||
|
||||
迁移规则如下:
|
||||
这两个入口经常被混用,但职责不同:
|
||||
|
||||
- 同一个设置类型的同一个 `FromVersion` 只能注册一个迁移器
|
||||
- `ToVersion` 必须严格大于 `FromVersion`
|
||||
- `InitializeAsync()` 会以当前运行时代码里该设置实例的 `Version` 作为目标版本
|
||||
- 如果迁移链缺口、迁移结果类型不兼容、迁移结果版本与声明不一致,或者读取到比当前运行时更高的版本,当前设置节不会覆盖内存中的最新实例,并会记录错误日志
|
||||
- 与 `SaveRepository<TSaveData>` 不同,设置初始化阶段会跳过失败的设置节并继续处理其他设置节,而不是把异常继续向外抛出
|
||||
- `GetData<T>()`
|
||||
- 只保证某个设置数据实例存在,并在 repository / location provider 已就绪时把类型注册回去
|
||||
- `RegisterApplicator(...)`
|
||||
- 同时注册 applicator 和 applicator 绑定的 `Data`
|
||||
|
||||
## 依赖项
|
||||
如果一个设置类型需要真正作用到运行时对象,推荐让它通过 applicator 进入模型;这样 `ApplyAllAsync()`、`ResetAll()` 和
|
||||
`SettingsSystem` 才能完整覆盖到它。
|
||||
|
||||
要让设置系统完整工作,通常需要准备:
|
||||
## 与 repository 的关系
|
||||
|
||||
- `ISettingsDataRepository`
|
||||
- `IDataLocationProvider`
|
||||
- 一个具体的存储实现和序列化器
|
||||
设置系统默认不是直接写文件,而是依赖 `ISettingsDataRepository`。
|
||||
|
||||
如果使用 `UnifiedSettingsDataRepository`,多个设置节会被合并到单个设置文件中统一保存。
|
||||
当前仓库里更推荐的默认实现是 `UnifiedSettingsDataRepository`,原因很直接:
|
||||
|
||||
- 多个设置 section 会被聚合到同一份统一文件
|
||||
- 启动时能一次性 `LoadAllAsync()`
|
||||
- `AutoBackup` 针对整个统一文件生效,更贴近“设置快照”的真实语义
|
||||
|
||||
如果你的项目明确需要“一类设置一个独立文件”,才考虑回到通用 `DataRepository` 路径。
|
||||
|
||||
## 当前边界
|
||||
|
||||
- 设置迁移是内建能力
|
||||
- 设置持久化是内建能力
|
||||
- 设置如何应用到具体引擎由 applicator 决定
|
||||
- 存档系统也支持内建版本迁移,但入口位于 `ISaveRepository<T>.RegisterMigration(...)`,语义是槽位存档升级而不是设置节初始化
|
||||
- `SettingsModel<TRepository>` 负责数据生命周期,`SettingsSystem` 负责系统级调用入口;两者不要混成一个巨型服务
|
||||
- applicator 决定“怎么把数据应用到宿主”,repository 决定“怎么保存数据”,两层职责不要互相侵入
|
||||
- 设置迁移和存档迁移是两条不同管线;后者看 [`data.md`](./data.md) 里的 `SaveRepository<TSaveData>`
|
||||
|
||||
## 继续阅读
|
||||
|
||||
1. [数据与存档系统](./data.md)
|
||||
2. [存储系统](./storage.md)
|
||||
3. [Game 入口](./index.md)
|
||||
|
||||
@ -1,735 +1,181 @@
|
||||
---
|
||||
title: 存储系统详解
|
||||
description: 存储系统提供了灵活的文件存储和作用域隔离功能,支持跨平台数据持久化。
|
||||
title: Game 存储系统
|
||||
description: 以当前 GFramework.Game 源码与持久化测试为准,说明 FileStorage 与 ScopedStorage 的职责、路径语义和复用方式。
|
||||
---
|
||||
|
||||
# 存储系统详解
|
||||
# Game 存储系统
|
||||
|
||||
## 概述
|
||||
`GFramework.Game` 在存储这一层只提供宿主无关的 `IStorage` 默认实现和作用域包装器。
|
||||
|
||||
存储系统是 GFramework.Game 中用于管理文件存储的核心组件。它提供了统一的存储接口,支持键值对存储、作用域隔离、目录操作等功能,让你可以轻松实现游戏数据的持久化。
|
||||
当前真正对外需要理解的入口只有两个:
|
||||
|
||||
存储系统采用装饰器模式设计,通过 `IStorage` 接口定义统一的存储操作,`FileStorage` 提供基于文件系统的实现,`ScopedStorage`
|
||||
提供作用域隔离功能。
|
||||
- `FileStorage`
|
||||
- 负责 `key -> 文件路径 -> 序列化内容` 的落盘读写
|
||||
- `ScopedStorage`
|
||||
- 负责给同一份底层存储加前缀作用域,避免不同子系统直接拼字符串抢同一片键空间
|
||||
|
||||
**主要特性**:
|
||||
它们不负责:
|
||||
|
||||
- 统一的键值对存储接口
|
||||
- 基于文件系统的持久化
|
||||
- 作用域隔离和命名空间管理
|
||||
- 线程安全的并发访问
|
||||
- 支持同步和异步操作
|
||||
- 目录和文件列举功能
|
||||
- 路径安全防护
|
||||
- 跨平台支持(包括 Godot)
|
||||
- 设置 section 的聚合语义
|
||||
- 存档槽位目录约定
|
||||
- 业务数据迁移
|
||||
|
||||
## 核心概念
|
||||
这些都属于上层 repository。
|
||||
|
||||
### 存储接口
|
||||
## 当前公开入口
|
||||
|
||||
`IStorage` 定义了统一的存储操作:
|
||||
### `FileStorage`
|
||||
|
||||
`FileStorage` 是 `IStorage` 的默认文件系统实现。按当前源码,它的职责比较集中:
|
||||
|
||||
- 把业务 key 映射到根目录下的层级文件路径
|
||||
- 通过构造函数注入的 `ISerializer` 负责对象序列化和反序列化
|
||||
- 对同一目标路径使用 `IAsyncKeyLockManager` 做细粒度串行化
|
||||
- 写入时先落 `.tmp` 临时文件,再原子替换目标文件
|
||||
- 自动创建父目录
|
||||
- 拒绝包含 `..` 的非法 key,并清理路径段中的非法文件名字符
|
||||
|
||||
默认文件扩展名是 `.dat`,也可以在构造时改成 `.json` 或其他后缀:
|
||||
|
||||
```csharp
|
||||
public interface IStorage : IUtility
|
||||
{
|
||||
// 检查键是否存在
|
||||
bool Exists(string key);
|
||||
Task<bool> ExistsAsync(string key);
|
||||
|
||||
// 读取数据
|
||||
T Read<T>(string key);
|
||||
T Read<T>(string key, T defaultValue);
|
||||
Task<T> ReadAsync<T>(string key);
|
||||
|
||||
// 写入数据
|
||||
void Write<T>(string key, T value);
|
||||
Task WriteAsync<T>(string key, T value);
|
||||
|
||||
// 删除数据
|
||||
void Delete(string key);
|
||||
Task DeleteAsync(string key);
|
||||
|
||||
// 目录操作
|
||||
Task<IReadOnlyList<string>> ListDirectoriesAsync(string path = "");
|
||||
Task<IReadOnlyList<string>> ListFilesAsync(string path = "");
|
||||
Task<bool> DirectoryExistsAsync(string path);
|
||||
Task CreateDirectoryAsync(string path);
|
||||
}
|
||||
```
|
||||
|
||||
### 文件存储
|
||||
|
||||
`FileStorage` 是基于文件系统的存储实现:
|
||||
|
||||
- 将数据序列化后保存为文件
|
||||
- 支持自定义文件扩展名(默认 `.dat`)
|
||||
- 使用细粒度锁保证线程安全
|
||||
- 自动创建目录结构
|
||||
- 防止路径遍历攻击
|
||||
|
||||
### 作用域存储
|
||||
|
||||
`ScopedStorage` 提供命名空间隔离:
|
||||
|
||||
- 为所有键添加前缀
|
||||
- 支持嵌套作用域
|
||||
- 透明包装底层存储
|
||||
- 实现逻辑分组
|
||||
|
||||
### 存储类型
|
||||
|
||||
`StorageKinds` 枚举定义了不同的存储方式:
|
||||
|
||||
```csharp
|
||||
[Flags]
|
||||
public enum StorageKinds
|
||||
{
|
||||
None = 0,
|
||||
Local = 1 << 0, // 本地文件系统
|
||||
Memory = 1 << 1, // 内存存储
|
||||
Remote = 1 << 2, // 远程存储
|
||||
Database = 1 << 3 // 数据库存储
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建文件存储
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Storage;
|
||||
using GFramework.Core.Abstractions.Storage;
|
||||
using GFramework.Game.Serializer;
|
||||
|
||||
// 创建序列化器
|
||||
var serializer = new JsonSerializer();
|
||||
|
||||
// 创Windows 示例)
|
||||
var storage = new FileStorage(@"C:\MyGame\Data", serializer);
|
||||
|
||||
// 或使用自定义扩展名
|
||||
var storage = new FileStorage(@"C:\MyGame\Data", serializer, ".json");
|
||||
```
|
||||
|
||||
### 写入和读取数据
|
||||
|
||||
```csharp
|
||||
// 写入简单类型
|
||||
storage.Write("player_score", 1000);
|
||||
storage.Write("player_name", "Alice");
|
||||
|
||||
// 写入复杂对象
|
||||
var settings = new GameSettings
|
||||
{
|
||||
Volume = 0.8f,
|
||||
Difficulty = "Hard",
|
||||
Language = "zh-CN"
|
||||
};
|
||||
storage.Write("settings", settings);
|
||||
|
||||
// 读取数据
|
||||
int score = storage.Read<int>("player_score");
|
||||
string name = storage.Read<string>("player_name");
|
||||
var loadedSettings = storage.Read<GameSettings>("settings");
|
||||
|
||||
// 读取数据(带默认值)
|
||||
int highScore = storage.Read("high_score", 0);
|
||||
```
|
||||
|
||||
### 异步操作
|
||||
|
||||
```csharp
|
||||
// 异步写入
|
||||
await storage.WriteAsync("player_level", 10);
|
||||
|
||||
// 异步读取
|
||||
int level = await storage.ReadAsync<int>("player_level");
|
||||
|
||||
// 异步检查存在
|
||||
bool exists = await storage.ExistsAsync("player_level");
|
||||
|
||||
// 异步删除
|
||||
await storage.DeleteAsync("player_level");
|
||||
```
|
||||
|
||||
### 检查和删除
|
||||
|
||||
```csharp
|
||||
// 检查键是否存在
|
||||
if (storage.Exists("player_score"))
|
||||
{
|
||||
Console.WriteLine("存档存在");
|
||||
}
|
||||
|
||||
// 删除数据
|
||||
storage.Delete("player_score");
|
||||
|
||||
// 异步检查
|
||||
bool exists = await storage.ExistsAsync("player_score");
|
||||
```
|
||||
|
||||
### 使用层级键
|
||||
|
||||
```csharp
|
||||
// 使用 / 分隔符创建层级结构
|
||||
storage.Write("player/profile/name", "Alice");
|
||||
storage.Write("player/profile/level", 10);
|
||||
storage.Write("player/inventory/gold", 1000);
|
||||
|
||||
// 文件结构:
|
||||
// Data/
|
||||
// player/
|
||||
// profile/
|
||||
// name.dat
|
||||
// level.dat
|
||||
// inventory/
|
||||
// gold.dat
|
||||
|
||||
// 读取层级数据
|
||||
string name = storage.Read<string>("player/profile/name");
|
||||
int gold = storage.Read<int>("player/inventory/gold");
|
||||
```
|
||||
|
||||
## 作用域存储
|
||||
|
||||
### 创建作用域存储
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Storage;
|
||||
|
||||
// 基于文件存储创建作用域存储
|
||||
var baseStorage = new FileStorage(@"C:\MyGame\Data", serializer);
|
||||
var playerStorage = new ScopedStorage(baseStorage, "player");
|
||||
|
||||
// 所有操作都会添加 "player/" 前缀
|
||||
playerStorage.Write("name", "Alice"); // 实际存储为 "player/name.dat"
|
||||
playerStorage.Write("level", 10); // 实际存储为 "player/level.dat"
|
||||
|
||||
// 读取时也使用相同的前缀
|
||||
string name = playerStorage.Read<string>("name"); // 从 "player/name.dat" 读取
|
||||
var serializer = new JsonSerializer();
|
||||
IStorage storage = new FileStorage("GameData", serializer, ".json");
|
||||
```
|
||||
|
||||
### 嵌套作用域
|
||||
### `ScopedStorage`
|
||||
|
||||
`ScopedStorage` 不额外实现一套落盘逻辑,只是给底层 `IStorage` 包一层前缀。
|
||||
|
||||
它适合做的是:
|
||||
|
||||
- 把 `settings/`、`profiles/`、`runtime-cache/` 这类键空间隔离开
|
||||
- 让多个 repository 或 utility 共用同一份根存储
|
||||
- 避免项目层到处手写 `"settings/xxx"`、`"save/slot_1/xxx"` 之类的字符串拼接
|
||||
|
||||
当前实现还支持继续嵌套:
|
||||
|
||||
```csharp
|
||||
// 创建嵌套作用域
|
||||
var settingsStorage = new ScopedStorage(baseStorage, "settings");
|
||||
var graphicsStorage = new ScopedStorage(settingsStorage, "graphics");
|
||||
|
||||
// 前缀变为 "settings/graphics/"
|
||||
graphicsStorage.Write("resolution", "1920x1080");
|
||||
// 实际存储为 "settings/graphics/resolution.dat"
|
||||
|
||||
// 或使用 Scope 方法
|
||||
var rootStorage = new FileStorage("GameData", new JsonSerializer(), ".json");
|
||||
var settingsStorage = new ScopedStorage(rootStorage, "settings");
|
||||
var audioStorage = settingsStorage.Scope("audio");
|
||||
audioStorage.Write("volume", 0.8f);
|
||||
// 实际存储为 "settings/audio/volume.dat"
|
||||
|
||||
await audioStorage.WriteAsync("master", 0.8f);
|
||||
```
|
||||
|
||||
### 多作用域隔离
|
||||
最终实际写入的 key 会是 `settings/audio/master`。
|
||||
|
||||
```csharp
|
||||
// 创建不同作用域的存储
|
||||
var playerStorage = new ScopedStorage(baseStorage, "player");
|
||||
var gameStorage = new ScopedStorage(baseStorage, "game");
|
||||
var settingsStorage = new ScopedStorage(baseStorage, "settings");
|
||||
## 路径语义
|
||||
|
||||
// 在不同作用域中使用相同的键不会冲突
|
||||
playerStorage.Write("level", 5); // player/level.dat
|
||||
gameStorage.Write("level", "forest_area_1"); // game/level.dat
|
||||
settingsStorage.Write("level", "high"); // settings/level.dat
|
||||
### key 到文件路径的映射
|
||||
|
||||
// 读取时各自独立
|
||||
int playerLevel = playerStorage.Read<int>("level"); // 5
|
||||
string gameLevel = gameStorage.Read<string>("level"); // "forest_area_1"
|
||||
string settingsLevel = settingsStorage.Read<string>("level"); // "high"
|
||||
`FileStorage` 会把 key 中的 `/` 当成目录分隔符,把最后一段作为文件名,并自动附加扩展名。
|
||||
|
||||
例如:
|
||||
|
||||
```text
|
||||
key: profile/player
|
||||
root: GameData
|
||||
extension: .json
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
会落到:
|
||||
|
||||
### 目录操作
|
||||
```text
|
||||
GameData/profile/player.json
|
||||
```
|
||||
|
||||
这意味着 key 的语义应该保持“逻辑路径”,而不是“完整文件名”。不要在业务层再自己补一遍 `.json`,否则会得到双重后缀。
|
||||
|
||||
### 安全边界
|
||||
|
||||
当前实现会:
|
||||
|
||||
1. 把 `\` 统一成 `/`
|
||||
2. 拒绝包含 `..` 的 key
|
||||
3. 清理每个路径段中的非法文件名字符
|
||||
|
||||
这套规则能挡住明显的路径逃逸和非法文件名问题,但它不代替业务层做目录规划。哪些 key 属于设置、存档还是缓存,仍应由上层模块统一约定。
|
||||
|
||||
### 同步与异步 API
|
||||
|
||||
`Read`、`Write`、`Exists`、`Delete` 这些同步方法只是对异步 API 的阻塞包装。
|
||||
|
||||
在 UI 线程或带同步上下文的宿主中,优先使用:
|
||||
|
||||
- `ReadAsync<T>()`
|
||||
- `WriteAsync<T>()`
|
||||
- `ExistsAsync()`
|
||||
- `DeleteAsync()`
|
||||
|
||||
只有在无法继续异步传播时,再退回同步封装。
|
||||
|
||||
## 最小接入路径
|
||||
|
||||
如果你只想先拿到一个可复用的本地持久化底座,最短路径如下:
|
||||
|
||||
```csharp
|
||||
// 列举子目录
|
||||
var directories = await storage.ListDirectoriesAsync("player");
|
||||
foreach (var dir in directories)
|
||||
using GFramework.Core.Abstractions.Serializer;
|
||||
using GFramework.Core.Abstractions.Storage;
|
||||
using GFramework.Game.Serializer;
|
||||
using GFramework.Game.Storage;
|
||||
|
||||
var serializer = new JsonSerializer();
|
||||
IStorage storage = new FileStorage("GameData", serializer, ".json");
|
||||
|
||||
await storage.WriteAsync("profiles/player", new Dictionary<string, int>
|
||||
{
|
||||
Console.WriteLine($"目录: {dir}");
|
||||
}
|
||||
["level"] = 12
|
||||
});
|
||||
|
||||
// 列举文件
|
||||
var files = await storage.ListFilesAsync("player/inventory");
|
||||
foreach (var file in files)
|
||||
{
|
||||
Console.WriteLine($"文件: {file}");
|
||||
}
|
||||
|
||||
// 检查目录是否存在
|
||||
bool exists = await storage.DirectoryExistsAsync("player/quests");
|
||||
|
||||
// 创建目录
|
||||
await storage.CreateDirectoryAsync("player/achievements");
|
||||
var loaded = await storage.ReadAsync<Dictionary<string, int>>("profiles/player");
|
||||
```
|
||||
|
||||
### 批量操作
|
||||
如果项目里同时有设置、存档和运行时缓存,推荐先在组合根把作用域拆开:
|
||||
|
||||
```csharp
|
||||
public async Task SaveAllPlayerData(PlayerData player)
|
||||
{
|
||||
var playerStorage = new ScopedStorage(baseStorage, $"player_{player.Id}");
|
||||
var serializer = new JsonSerializer();
|
||||
var rootStorage = new FileStorage("GameData", serializer, ".json");
|
||||
|
||||
// 批量写入
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
playerStorage.WriteAsync("profile", player.Profile),
|
||||
playerStorage.WriteAsync("inventory", player.Inventory),
|
||||
playerStorage.WriteAsync("quests", player.Quests),
|
||||
playerStorage.WriteAsync("achievements", player.Achievements)
|
||||
};
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
Console.WriteLine("所有玩家数据已保存");
|
||||
}
|
||||
|
||||
public async Task<PlayerData> LoadAllPlayerData(int playerId)
|
||||
{
|
||||
var playerStorage = new ScopedStorage(baseStorage, $"player_{playerId}");
|
||||
|
||||
// 批量读取
|
||||
var tasks = new[]
|
||||
{
|
||||
playerStorage.ReadAsync<Profile>("profile"),
|
||||
playerStorage.ReadAsync<Inventory>("inventory"),
|
||||
playerStorage.ReadAsync<QuestData>("quests"),
|
||||
playerStorage.ReadAsync<Achievements>("achievements")
|
||||
};
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
return new PlayerData
|
||||
{
|
||||
Id = playerId,
|
||||
Profile = tasks[0].Result,
|
||||
Inventory = tasks[1].Result,
|
||||
Quests = tasks[2].Result,
|
||||
Achievements = tasks[3].Result
|
||||
};
|
||||
}
|
||||
var settingsStorage = new ScopedStorage(rootStorage, "settings");
|
||||
var saveStorage = new ScopedStorage(rootStorage, "saves");
|
||||
var cacheStorage = new ScopedStorage(rootStorage, "runtime-cache");
|
||||
```
|
||||
|
||||
### 存储迁移
|
||||
不过在默认仓库接法里,项目通常不需要直接创建 `saveStorage` 这种 scoped instance,因为 `SaveRepository<TSaveData>`
|
||||
会再根据 `SaveConfiguration` 自己组织槽位目录。
|
||||
|
||||
```csharp
|
||||
public async Task MigrateStorage(IStorage oldStorage, IStorage newStorage, string path = "")
|
||||
{
|
||||
// 列举所有文件
|
||||
var files = await oldStorage.ListFilesAsync(path);
|
||||
## 与上层 repository 的关系
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var key = string.IsNullOrEmpty(path) ? file : $"{path}/{file}";
|
||||
`FileStorage` / `ScopedStorage` 是持久化最底层,不是最终采用入口。当前更常见的实际分工是:
|
||||
|
||||
// 读取旧数据
|
||||
var data = await oldStorage.ReadAsync<object>(key);
|
||||
- `DataRepository`
|
||||
- 每个 `IDataLocation` 对应一份独立持久化对象
|
||||
- `UnifiedSettingsDataRepository`
|
||||
- 把多个设置 section 聚合到同一个统一文件里保存
|
||||
- `SaveRepository<TSaveData>`
|
||||
- 负责存档槽位、文件名和迁移链
|
||||
|
||||
// 写入新存储
|
||||
await newStorage.WriteAsync(key, data);
|
||||
也就是说:
|
||||
|
||||
Console.WriteLine($"已迁移: {key}");
|
||||
}
|
||||
- 业务层如果想保存一份独立数据,优先看 [`data.md`](./data.md)
|
||||
- 业务层如果想保存设置,优先看 [`setting.md`](./setting.md)
|
||||
- 业务层如果只是需要底层存储实现,才直接依赖 `IStorage`
|
||||
|
||||
// 递归处理子目录
|
||||
var directories = await oldStorage.ListDirectoriesAsync(path);
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
var subPath = string.IsNullOrEmpty(path) ? dir : $"{path}/{dir}";
|
||||
await MigrateStorage(oldStorage, newStorage, subPath);
|
||||
}
|
||||
}
|
||||
```
|
||||
## 当前边界
|
||||
|
||||
### 存储备份
|
||||
- `FileStorage` 已经会通过注入的 `ISerializer` 自动序列化对象;默认接法不需要先手工 `Serialize(...)` 再把字符串写回
|
||||
- `FileStorage` 负责目录列举与目录创建,但不负责“列出所有存档槽位”的业务语义
|
||||
- `ScopedStorage` 只做 key 前缀,不做权限、事务或迁移控制
|
||||
- 锁粒度是“当前实例内的目标路径”,不是跨进程文件锁
|
||||
- 原子写入只覆盖单文件替换,不等于多文件事务
|
||||
|
||||
```csharp
|
||||
public class StorageBackupSystem
|
||||
{
|
||||
private readonly IStorage _storage;
|
||||
private readonly string _backupPrefix = "backup";
|
||||
## 继续阅读
|
||||
|
||||
public async Task CreateBackup(string sourcePath)
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
var backupPath = $"{_backupPrefix}/{timestamp}";
|
||||
|
||||
await CopyDirectory(sourcePath, backupPath);
|
||||
Console.WriteLine($"备份已创建: {backupPath}");
|
||||
}
|
||||
|
||||
public async Task RestoreBackup(string backupName, string targetPath)
|
||||
{
|
||||
var backupPath = $"{_backupPrefix}/{backupName}";
|
||||
|
||||
if (!await _storage.DirectoryExistsAsync(backupPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"备份不存在: {backupName}");
|
||||
}
|
||||
|
||||
await CopyDirectory(backupPath, targetPath);
|
||||
Console.WriteLine($"已从备份恢复: {backupName}");
|
||||
}
|
||||
|
||||
private async Task CopyDirectory(string source, string target)
|
||||
{
|
||||
var files = await _storage.ListFilesAsync(source);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var sourceKey = $"{source}/{file}";
|
||||
var targetKey = $"{target}/{file}";
|
||||
var data = await _storage.ReadAsync<object>(sourceKey);
|
||||
await _storage.WriteAsync(targetKey, data);
|
||||
}
|
||||
|
||||
var directories = await _storage.ListDirectoriesAsync(source);
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
await CopyDirectory($"{source}/{dir}", $"{target}/{dir}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存层
|
||||
|
||||
```csharp
|
||||
public class CachedStorage : IStorage
|
||||
{
|
||||
private readonly IStorage _innerStorage;
|
||||
private readonly ConcurrentDictionary<string, object> _cache = new();
|
||||
|
||||
public CachedStorage(IStorage innerStorage)
|
||||
{
|
||||
_innerStorage = innerStorage;
|
||||
}
|
||||
|
||||
public T Read<T>(string key)
|
||||
{
|
||||
// 先从缓存读取
|
||||
if (_cache.TryGetValue(key, out var cached))
|
||||
{
|
||||
return (T)cached;
|
||||
}
|
||||
|
||||
// 从存储读取并缓存
|
||||
var value = _innerStorage.Read<T>(key);
|
||||
_cache[key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
public void Write<T>(string key, T value)
|
||||
{
|
||||
// 写入存储
|
||||
_innerStorage.Write(key, value);
|
||||
|
||||
// 更新缓存
|
||||
_cache[key] = value;
|
||||
}
|
||||
|
||||
public void Delete(string key)
|
||||
{
|
||||
_innerStorage.Delete(key);
|
||||
_cache.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Godot 集成
|
||||
|
||||
### 使用 Godot 文件存储
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.Storage;
|
||||
|
||||
// 创建 Godot 文件存储
|
||||
var storage = new GodotFileStorage(serializer);
|
||||
|
||||
// 使用 user:// 路径(用户数据目录)
|
||||
storage.Write("user://saves/slot1.dat", saveData);
|
||||
var data = storage.Read<SaveData>("user://saves/slot1.dat");
|
||||
|
||||
// 使用 res:// 路径(资源目录,只读)
|
||||
var config = storage.Read<Config>("res://config/default.json");
|
||||
|
||||
// 普通文件路径也支持
|
||||
storage.Write("/tmp/temp_data.dat", tempData);
|
||||
```
|
||||
|
||||
### Godot 路径说明
|
||||
|
||||
```csharp
|
||||
// user:// - 用户数据目录
|
||||
// Windows: %APPDATA%/Godot/app_userdata/[project_name]
|
||||
// Linux: ~/.local/share/godot/app_userdata/[project_name]
|
||||
// macOS: ~/Library/Application Support/Godot/app_userdata/[project_name]
|
||||
storage.Write("user://save.dat", data);
|
||||
|
||||
// res:// - 项目资源目录(只读)
|
||||
var config = storage.Read<Config>("res://data/config.json");
|
||||
|
||||
// 绝对路径
|
||||
storage.Write("/home/user/game/data.dat", data);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用作用域隔离不同类型的数据**
|
||||
```csharp
|
||||
✓ var playerStorage = new ScopedStorage(baseStorage, "player");
|
||||
✓ var settingsStorage = new ScopedStorage(baseStorage, "settings");
|
||||
✗ storage.Write("player_name", name); // 不使用作用域
|
||||
```
|
||||
|
||||
2. **使用异步操作避免阻塞**
|
||||
```csharp
|
||||
✓ await storage.WriteAsync("data", value);
|
||||
✗ storage.Write("data", value); // 在 UI 线程中同步操作
|
||||
```
|
||||
|
||||
3. **读取时提供默认值**
|
||||
```csharp
|
||||
✓ int score = storage.Read("score", 0);
|
||||
✗ int score = storage.Read<int>("score"); // 键不存在时抛异常
|
||||
```
|
||||
|
||||
4. **使用层级键组织数据**
|
||||
```csharp
|
||||
✓ storage.Write("player/inventory/gold", 1000);
|
||||
✗ storage.Write("player_inventory_gold", 1000);
|
||||
```
|
||||
|
||||
5. **处理存储异常**
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
await storage.WriteAsync("data", value);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.Error($"存储失败: {ex.Message}");
|
||||
ShowErrorMessage("保存失败,请检查磁盘空间");
|
||||
}
|
||||
```
|
||||
|
||||
6. **定期清理过期数据**
|
||||
```csharp
|
||||
public async Task CleanupOldData(TimeSpan maxAge)
|
||||
{
|
||||
var files = await storage.ListFilesAsync("temp");
|
||||
foreach (var file in files)
|
||||
{
|
||||
var data = await storage.ReadAsync<TimestampedData>($"temp/{file}");
|
||||
if (DateTime.Now - data.Timestamp > maxAge)
|
||||
{
|
||||
await storage.DeleteAsync($"temp/{file}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
7. **使用合适的序列化器**
|
||||
```csharp
|
||||
// JSON - 可读性好,适合配置文件
|
||||
var jsonStorage = new FileStorage(path, new JsonSerializer(), ".json");
|
||||
|
||||
// 二进制 - 性能好,适合大量数据
|
||||
var binaryStorage = new FileStorage(path, new BinarySerializer(), ".dat");
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何实现跨平台存储路径?
|
||||
|
||||
**解答**:
|
||||
使用 `Environment.GetFolderPath` 获取平台特定路径:
|
||||
|
||||
```csharp
|
||||
public static string GetStoragePath()
|
||||
{
|
||||
var appData = Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.ApplicationData);
|
||||
return Path.Combine(appData, "MyGame", "Data");
|
||||
}
|
||||
|
||||
var storage = new FileStorage(GetStoragePath(), serializer);
|
||||
```
|
||||
|
||||
### 问题:存储系统是否线程安全?
|
||||
|
||||
**解答**:
|
||||
是的,`FileStorage` 使用细粒度锁机制保证线程安全:
|
||||
|
||||
```csharp
|
||||
// 不同键的操作可以并发执行
|
||||
Task.Run(() => storage.Write("key1", value1));
|
||||
Task.Run(() => storage.Write("key2", value2));
|
||||
|
||||
// 相同键的操作会串行化
|
||||
Task.Run(() => storage.Write("key", value1));
|
||||
Task.Run(() => storage.Write("key", value2)); // 等待第一个完成
|
||||
```
|
||||
|
||||
### 问题:如何实现存储加密?
|
||||
|
||||
**解答**:
|
||||
创建加密存储包装器:
|
||||
|
||||
```csharp
|
||||
public class EncryptedStorage : IStorage
|
||||
{
|
||||
private readonly IStorage _innerStorage;
|
||||
private readonly IEncryption _encryption;
|
||||
|
||||
public void Write<T>(string key, T value)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(value);
|
||||
var encrypted = _encryption.Encrypt(json);
|
||||
_innerStorage.Write(key, encrypted);
|
||||
}
|
||||
|
||||
public T Read<T>(string key)
|
||||
{
|
||||
var encrypted = _innerStorage.Read<byte[]>(key);
|
||||
var json = _encryption.Decrypt(encrypted);
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何限制存储大小?
|
||||
|
||||
**解答**:
|
||||
实现配额管理:
|
||||
|
||||
```csharp
|
||||
public class QuotaStorage : IStorage
|
||||
{
|
||||
private readonly IStorage _innerStorage;
|
||||
private readonly long _maxSize;
|
||||
private long _currentSize;
|
||||
|
||||
public void Write<T>(string key, T value)
|
||||
{
|
||||
var data = Serialize(value);
|
||||
var size = data.Length;
|
||||
|
||||
if (_currentSize + size > _maxSize)
|
||||
{
|
||||
throw new InvalidOperationException("存储配额已满");
|
||||
}
|
||||
|
||||
_innerStorage.Write(key, value);
|
||||
_currentSize += size;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何实现存储压缩?
|
||||
|
||||
**解答**:
|
||||
使用压缩序列化器:
|
||||
|
||||
```csharp
|
||||
public class CompressedSerializer : ISerializer
|
||||
{
|
||||
private readonly ISerializer _innerSerializer;
|
||||
|
||||
public string Serialize<T>(T value)
|
||||
{
|
||||
var json = _innerSerializer.Serialize(value);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var compressed = Compress(bytes);
|
||||
return Convert.ToBase64String(compressed);
|
||||
}
|
||||
|
||||
public T Deserialize<T>(string data)
|
||||
{
|
||||
var compressed = Convert.FromBase64String(data);
|
||||
var bytes = Decompress(compressed);
|
||||
var json = Encoding.UTF8.GetString(bytes);
|
||||
return _innerSerializer.Deserialize<T>(json);
|
||||
}
|
||||
|
||||
private byte[] Compress(byte[] data)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
using (var gzip = new GZipStream(output, CompressionMode.Compress))
|
||||
{
|
||||
gzip.Write(data, 0, data.Length);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private byte[] Decompress(byte[] data)
|
||||
{
|
||||
using var input = new MemoryStream(data);
|
||||
using var gzip = new GZipStream(input, CompressionMode.Decompress);
|
||||
using var output = new MemoryStream();
|
||||
gzip.CopyTo(output);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何监控存储操作?
|
||||
|
||||
**解答**:
|
||||
实现日志存储包装器:
|
||||
|
||||
```csharp
|
||||
public class LoggingStorage : IStorage
|
||||
{
|
||||
private readonly IStorage _innerStorage;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public void Write<T>(string key, T value)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
_innerStorage.Write(key, value);
|
||||
_logger.Info($"写入成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"写入失败: {key}, 错误: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public T Read<T>(string key)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var value = _innerStorage.Read<T>(key);
|
||||
_logger.Info($"读取成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
|
||||
return value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"读取失败: {key}, 错误: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
|
||||
- [序列化系统](/zh-CN/game/serialization) - 数据序列化
|
||||
- [Godot 集成](/zh-CN/godot/index) - Godot 中的存储
|
||||
- [存档系统教程](/zh-CN/tutorials/save-system) - 完整示例
|
||||
1. [数据与存档系统](./data.md)
|
||||
2. [设置系统](./setting.md)
|
||||
3. [序列化系统](./serialization.md)
|
||||
4. [Game 入口](./index.md)
|
||||
|
||||
@ -1,603 +1,156 @@
|
||||
# Godot 设置模块 (Godot Settings Module)
|
||||
---
|
||||
title: Godot 设置系统
|
||||
description: 以当前 GFramework.Godot 源码、测试与 CoreGrid 接线为准,说明 Godot settings applicator 的职责、注册方式和运行时边界。
|
||||
---
|
||||
|
||||
## 概述
|
||||
# Godot 设置系统
|
||||
|
||||
Godot 设置模块是 GFramework.Godot 的核心组件之一,专门为 Godot 引擎提供游戏设置系统的实现。该模块将通用的设置框架与 Godot
|
||||
引擎的特定功能相结合,提供了音频设置、图形设置和本地化设置的完整解决方案。
|
||||
`GFramework.Godot` 在设置这一层做的事情很克制:它没有重新发明一套设置模型,而是给
|
||||
`GFramework.Game` 的 `ISettingsModel` 提供三个 Godot 宿主 applicator:
|
||||
|
||||
## 核心类
|
||||
- `GodotAudioSettings`
|
||||
- `GodotGraphicsSettings`
|
||||
- `GodotLocalizationSettings`
|
||||
|
||||
### 音频设置系统
|
||||
这些类型的职责是“把已经存在的设置数据应用到 Godot 引擎和框架运行时”,不是负责设置 UI、设置持久化或设置迁移。
|
||||
|
||||
#### AudioBusMap
|
||||
## 当前公开入口
|
||||
|
||||
音频总线映射配置类,用于定义音频系统中不同类型音频的总线名称。
|
||||
### `GodotAudioSettings`
|
||||
|
||||
**属性:**
|
||||
`GodotAudioSettings` 从 `ISettingsModel` 读取 `AudioSettings`,再按 `AudioBusMap` 中的总线名把音量写入
|
||||
`AudioServer`。
|
||||
|
||||
- `Master` - 主音频总线名称(默认:"Master")
|
||||
- `Bgm` - 背景音乐音频总线名称(默认:"BGM")
|
||||
- `Sfx` - 音效音频总线名称(默认:"SFX")
|
||||
当前行为有几个关键点:
|
||||
|
||||
#### GodotAudioApplier
|
||||
- `Master`、`Bgm`、`Sfx` 三类音量都来自 `AudioSettings`
|
||||
- 应用前会把线性音量限制在 `0.0001f ~ 1f`,再转换成分贝
|
||||
- 如果找不到对应 bus,当前实现只会 `GD.PushWarning(...)`,不会抛异常中断整个设置流程
|
||||
|
||||
音频设置应用器,负责将音频设置应用到 Godot 引擎的音频总线系统。
|
||||
`AudioBusMap` 默认值是:
|
||||
|
||||
**功能:**
|
||||
- `Master`
|
||||
- `BGM`
|
||||
- `SFX`
|
||||
|
||||
- 应用音量设置到指定音频总线
|
||||
- 处理音量格式转换(线性值到分贝)
|
||||
- 音频总线存在性检查和警告
|
||||
如果项目里的 Godot Audio Bus 命名不同,需要在注册 applicator 时替换映射,而不是改写 applicator 本身。
|
||||
|
||||
#### GodotAudioSettings
|
||||
### `GodotGraphicsSettings`
|
||||
|
||||
Godot 音频设置实现类,接收 AudioSettings 配置并实现 IApplyAbleSettings 接口,负责将音频配置应用到 Godot 音频系统。
|
||||
`GodotGraphicsSettings` 从 `ISettingsModel` 读取 `GraphicsSettings`,并把结果同步到 `DisplayServer`:
|
||||
|
||||
**实现关系:**
|
||||
- `Fullscreen = true` 时切到 `ExclusiveFullscreen`
|
||||
- 同时把 `Borderless` flag 设为 `true`
|
||||
- `Fullscreen = false` 时切回窗口模式,设置窗口尺寸,并按主屏尺寸重新居中
|
||||
|
||||
```
|
||||
AudioSettings (配置数据)
|
||||
↓ [组合]
|
||||
GodotAudioSettings (Godot 特定实现) → IApplyAbleSettings (可应用设置接口)
|
||||
```
|
||||
当前实现没有扩展到分辨率档位之外的图形质量、渲染后端或平台特定显示策略。本页不再把这些未实现能力写成既成事实。
|
||||
|
||||
**功能:**
|
||||
### `GodotLocalizationSettings`
|
||||
|
||||
- 接收 AudioSettings 配置对象和 AudioBusMap 总线映射
|
||||
- 实现 `ApplyAsync()` 方法,将音量设置应用到指定音频总线
|
||||
- 支持自定义音频总线映射
|
||||
- 自动处理音量格式转换(线性值到分贝)
|
||||
`GodotLocalizationSettings` 负责把 `LocalizationSettings.Language` 同时同步到:
|
||||
|
||||
### 图形设置系统
|
||||
- Godot `TranslationServer.SetLocale(...)`
|
||||
- GFramework `ILocalizationManager.SetLanguage(...)`
|
||||
|
||||
#### GodotGraphicsSettings
|
||||
这一步依赖 `LocalizationMap` 把“用户可见语言值”拆成两套目标值:
|
||||
|
||||
Godot 图形设置实现类,继承自 GraphicsSettings 并实现 IApplyAbleSettings。
|
||||
- Godot locale,例如 `zh_CN`
|
||||
- 框架语言码,例如 `zhs`
|
||||
|
||||
**功能:**
|
||||
当前默认映射是:
|
||||
|
||||
- 分辨率设置和窗口尺寸调整
|
||||
- 全屏模式切换
|
||||
- 窗口位置自动居中
|
||||
- 多显示器支持
|
||||
- `简体中文` -> Godot `zh_CN`,框架 `zhs`
|
||||
- `English` -> Godot `en`,框架 `eng`
|
||||
|
||||
### 本地化设置系统
|
||||
`GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs` 已覆盖三条关键边界:
|
||||
|
||||
#### LocalizationMap
|
||||
- 英文会同步到 `en` / `eng`
|
||||
- 简体中文会同步到 `zh_CN` / `zhs`
|
||||
- 未知语言值会稳定回退到英文,而不是让 Godot locale 与框架语言状态分裂
|
||||
|
||||
本地化映射配置类,用于把设置系统中保存的用户可见语言值解析为:
|
||||
如果当前架构上下文里解析不到 `ILocalizationManager`,Godot locale 仍会被设置,只是不会额外同步框架语言管理器。
|
||||
|
||||
- Godot `TranslationServer` 使用的 locale
|
||||
- GFramework `ILocalizationManager` 使用的语言码
|
||||
## 最小接入路径
|
||||
|
||||
默认映射如下:
|
||||
|
||||
- `"简体中文"` -> Godot `zh_CN`,框架语言码 `zhs`
|
||||
- `"English"` -> Godot `en`,框架语言码 `eng`
|
||||
|
||||
未知语言值会稳定回退到英文,避免重启后出现设置值与运行时语言状态不一致。
|
||||
|
||||
#### GodotLocalizationSettings
|
||||
|
||||
Godot 本地化设置实现类,负责把 `LocalizationSettings` 同时应用到 Godot 引擎与 GFramework 本地化管理器。
|
||||
|
||||
**功能:**
|
||||
|
||||
- 将语言设置应用到 `TranslationServer.SetLocale(...)`
|
||||
- 同步 `ILocalizationManager.SetLanguage(...)`
|
||||
- 通过统一映射避免 Godot locale 与框架语言码分裂
|
||||
|
||||
## 架构设计
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[AudioSettings] --> B[GodotAudioSettings]
|
||||
C[GraphicsSettings] --> D[GodotGraphicsSettings]
|
||||
E[LocalizationSettings] --> F[GodotLocalizationSettings]
|
||||
G[IApplyAbleSettings] --> B
|
||||
G --> D
|
||||
G --> F
|
||||
|
||||
H[AudioBusMap] --> B
|
||||
I[LocalizationMap] --> F
|
||||
|
||||
B --> J[AudioServer API]
|
||||
D --> K[DisplayServer API]
|
||||
F --> L[TranslationServer API]
|
||||
F --> M[ILocalizationManager]
|
||||
|
||||
N[SettingsSystem] --> O[ApplyAsync Method]
|
||||
O --> B
|
||||
O --> D
|
||||
O --> F
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 音频设置配置
|
||||
|
||||
#### 基本音频设置
|
||||
当前消费者 `ai-libs/CoreGrid` 的接法,是先注册 `SettingsModel<ISettingsDataRepository>`,再把 Godot applicator
|
||||
挂进去:
|
||||
|
||||
```csharp
|
||||
// 创建音频配置数据
|
||||
var settings = new AudioSettings
|
||||
{
|
||||
MasterVolume = 0.8f, // 80% 主音量
|
||||
BgmVolume = 0.6f, // 60% 背景音乐音量
|
||||
SfxVolume = 0.9f // 90% 音效音量
|
||||
};
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Abstractions.Setting;
|
||||
using GFramework.Game.Setting;
|
||||
using GFramework.Godot.Setting;
|
||||
using GFramework.Godot.Setting.Data;
|
||||
|
||||
// 创建 Godot 音频设置应用器
|
||||
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
||||
var settingsDataRepository = architecture.Context.GetUtility<ISettingsDataRepository>();
|
||||
|
||||
// 应用设置
|
||||
await audioSettings.ApplyAsync();
|
||||
architecture.RegisterModel(
|
||||
new SettingsModel<ISettingsDataRepository>(
|
||||
new SettingDataLocationProvider(),
|
||||
settingsDataRepository)
|
||||
.Also(it =>
|
||||
{
|
||||
it.RegisterApplicator(new GodotAudioSettings(it, new AudioBusMap()))
|
||||
.RegisterApplicator(new GodotGraphicsSettings(it))
|
||||
.RegisterApplicator(new GodotLocalizationSettings(it, new LocalizationMap()));
|
||||
}));
|
||||
```
|
||||
|
||||
#### 自定义音频总线映射
|
||||
这条接法说明了当前边界:
|
||||
|
||||
- 设置数据和生命周期由 `SettingsModel` 管
|
||||
- `GodotAudioSettings` / `GodotGraphicsSettings` / `GodotLocalizationSettings` 只是 applicator
|
||||
- 保存、加载和迁移仍然走 `ISettingsDataRepository`、`SettingsModel.InitializeAsync()`、`SaveAllAsync()` 等 `Game`
|
||||
family 入口
|
||||
|
||||
## 运行时使用方式
|
||||
|
||||
业务代码通常不会直接 new 一次 applicator 然后立即调用,而是通过 `ISettingsSystem` 或 `ISettingsModel` 触发应用:
|
||||
|
||||
```csharp
|
||||
// 自定义音频总线映射
|
||||
var customBusMap = new AudioBusMap
|
||||
{
|
||||
Master = "Master_Bus",
|
||||
Bgm = "Background_Music",
|
||||
Sfx = "Sound_Effects"
|
||||
};
|
||||
using GFramework.Game.Abstractions.Setting;
|
||||
using GFramework.Godot.Setting;
|
||||
|
||||
// 创建音频配置
|
||||
var settings = new AudioSettings
|
||||
{
|
||||
MasterVolume = 0.7f,
|
||||
BgmVolume = 0.5f,
|
||||
SfxVolume = 0.8f
|
||||
};
|
||||
|
||||
// 使用自定义总线映射应用设置
|
||||
var audioSettings = new GodotAudioSettings(settings, customBusMap);
|
||||
await audioSettings.ApplyAsync();
|
||||
```
|
||||
|
||||
#### 通过设置系统使用
|
||||
|
||||
```csharp
|
||||
// 注册音频设置到设置模型
|
||||
var settingsModel = this.GetModel<ISettingsModel>();
|
||||
var audioSettingsData = settingsModel.Get<AudioSettings>();
|
||||
audioSettingsData.MasterVolume = 0.8f;
|
||||
audioSettingsData.BgmVolume = 0.6f;
|
||||
audioSettingsData.SfxVolume = 0.9f;
|
||||
var audioData = settingsModel.GetData<AudioSettings>();
|
||||
audioData.MasterVolume = 0.8f;
|
||||
audioData.BgmVolume = 0.6f;
|
||||
audioData.SfxVolume = 0.9f;
|
||||
|
||||
// 创建 Godot 音频设置应用器
|
||||
var godotAudioSettings = new GodotAudioSettings(audioSettingsData, new AudioBusMap());
|
||||
await godotAudioSettings.ApplyAsync();
|
||||
var settingsSystem = this.GetSystem<ISettingsSystem>();
|
||||
await settingsSystem.Apply<GodotAudioSettings>();
|
||||
```
|
||||
|
||||
### 图形设置配置
|
||||
对图形和语言设置的调用方式相同,区别只是 applicator 类型不同。
|
||||
|
||||
#### 基本图形设置
|
||||
## 当前边界
|
||||
|
||||
```csharp
|
||||
// 创建图形设置
|
||||
var graphicsSettings = new GodotGraphicsSettings
|
||||
{
|
||||
ResolutionWidth = 1920,
|
||||
ResolutionHeight = 1080,
|
||||
Fullscreen = true
|
||||
};
|
||||
- 这三个类型都不是设置数据对象;它们读取的是 `AudioSettings`、`GraphicsSettings`、`LocalizationSettings`
|
||||
- 它们不负责设置持久化;是否保存到文件由 `ISettingsDataRepository` 和存储层决定
|
||||
- `ApplyAsync()` 当前都只是同步推进 Godot 引擎调用后返回 `Task.CompletedTask`,不会启动后台工作线程
|
||||
- `GodotAudioSettings` 依赖项目里已经存在对应 bus 名称;缺失时只会警告,不会帮你自动创建总线
|
||||
- `GodotGraphicsSettings` 当前只覆盖窗口模式、尺寸和居中,不等于一个完整的图形选项系统
|
||||
- `GodotLocalizationSettings` 解决的是“用户语言值 -> Godot locale / 框架语言码”双向对齐,不负责翻译资源本身的组织方式
|
||||
|
||||
// 应用设置
|
||||
await graphicsSettings.ApplyAsync();
|
||||
```
|
||||
## 什么时候应该改看别的入口
|
||||
|
||||
#### 窗口模式切换
|
||||
### 先理解设置模型和仓库
|
||||
|
||||
```csharp
|
||||
public class DisplayManager : Node
|
||||
{
|
||||
private GodotGraphicsSettings _graphicsSettings;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_graphicsSettings = new GodotGraphicsSettings();
|
||||
}
|
||||
|
||||
public async Task ToggleFullscreen()
|
||||
{
|
||||
_graphicsSettings.Fullscreen = !_graphicsSettings.Fullscreen;
|
||||
await _graphicsSettings.ApplyAsync();
|
||||
}
|
||||
|
||||
public async Task SetResolution(int width, int height)
|
||||
{
|
||||
_graphicsSettings.ResolutionWidth = width;
|
||||
_graphicsSettings.ResolutionHeight = height;
|
||||
_graphicsSettings.Fullscreen = false; // 窗口化时自动关闭全屏
|
||||
await _graphicsSettings.ApplyAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
如果你想先理解 `ISettingsData`、`IResetApplyAbleSettings`、`SettingsModel`、`SettingsSystem` 与设置迁移,先看
|
||||
[`../game/setting.md`](../game/setting.md)。
|
||||
|
||||
#### 预设分辨率配置
|
||||
### 先理解设置如何被持久化
|
||||
|
||||
```csharp
|
||||
public class ResolutionPresets
|
||||
{
|
||||
public static readonly (int width, int height)[] CommonResolutions =
|
||||
{
|
||||
(1920, 1080), // Full HD
|
||||
(2560, 1440), // QHD
|
||||
(3840, 2160), // 4K
|
||||
(1280, 720), // HD
|
||||
(1366, 768), // 常见笔记本分辨率
|
||||
};
|
||||
|
||||
public static async Task ApplyResolution(GodotGraphicsSettings settings, int width, int height)
|
||||
{
|
||||
settings.ResolutionWidth = width;
|
||||
settings.ResolutionHeight = height;
|
||||
settings.Fullscreen = false;
|
||||
await settings.ApplyAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
如果你关注的是统一设置文件、备份、数据位置和底层存储实现,应该回到:
|
||||
|
||||
## API 详细说明
|
||||
- [`../game/storage.md`](../game/storage.md)
|
||||
- [Godot 存储系统](./storage.md)
|
||||
|
||||
### AudioBusMap
|
||||
本页只补 Godot 宿主如何“应用”设置,不重复维护一份完整设置系统手册。
|
||||
|
||||
```csharp
|
||||
public sealed class AudioBusMap
|
||||
{
|
||||
public string Master { get; init; } = "Master";
|
||||
public string Bgm { get; init; } = "BGM";
|
||||
public string Sfx { get; init; } = "SFX";
|
||||
}
|
||||
```
|
||||
## 继续阅读
|
||||
|
||||
**特点:**
|
||||
|
||||
- 使用 `init` 属性,创建后不可修改
|
||||
- 提供合理的默认值
|
||||
- 支持对象初始化语法
|
||||
|
||||
### GodotAudioSettings
|
||||
|
||||
```csharp
|
||||
public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
|
||||
{
|
||||
public Task ApplyAsync();
|
||||
}
|
||||
```
|
||||
|
||||
**构造函数参数:**
|
||||
|
||||
- `settings` - AudioSettings 配置对象,包含音量设置
|
||||
- `busMap` - AudioBusMap 对象,定义音频总线映射
|
||||
|
||||
**Apply 方法实现:**
|
||||
|
||||
```csharp
|
||||
public Task ApplyAsync()
|
||||
{
|
||||
SetBus(busMap.Master, settings.MasterVolume);
|
||||
SetBus(busMap.Bgm, settings.BgmVolume);
|
||||
SetBus(busMap.Sfx, settings.SfxVolume);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
```
|
||||
|
||||
### GodotGraphicsSettings
|
||||
|
||||
```csharp
|
||||
public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings
|
||||
{
|
||||
public Task ApplyAsync();
|
||||
}
|
||||
```
|
||||
|
||||
**Apply 方法功能:**
|
||||
|
||||
- 设置窗口边框标志
|
||||
- 切换窗口模式(窗口化/全屏)
|
||||
- 调整窗口尺寸
|
||||
- 自动居中窗口
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### 音频音量转换
|
||||
|
||||
Godot 音频系统使用分贝(dB)作为音量单位,而我们通常使用线性值(0-1):
|
||||
|
||||
```csharp
|
||||
// 线性值到分贝转换
|
||||
float linearVolume = 0.5f; // 50% 音量
|
||||
float dbVolume = Mathf.LinearToDb(linearVolume); // 转换为分贝
|
||||
|
||||
// 应用到音频总线
|
||||
AudioServer.SetBusVolumeDb(busIndex, dbVolume);
|
||||
```
|
||||
|
||||
### 音量限制和保护
|
||||
|
||||
为避免完全静音(-inf dB),应用了最小音量限制:
|
||||
|
||||
```csharp
|
||||
float clampedVolume = Mathf.Clamp(linear, 0.0001f, 1f);
|
||||
float dbVolume = Mathf.LinearToDb(clampedVolume);
|
||||
```
|
||||
|
||||
### 窗口管理
|
||||
|
||||
#### 全屏模式
|
||||
|
||||
```csharp
|
||||
// 设置全屏
|
||||
DisplayServer.WindowSetMode(DisplayServer.WindowMode.ExclusiveFullscreen);
|
||||
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, true);
|
||||
```
|
||||
|
||||
#### 窗口化模式
|
||||
|
||||
```csharp
|
||||
// 设置窗口化
|
||||
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed);
|
||||
DisplayServer.WindowSetSize(newSize);
|
||||
|
||||
// 居中窗口
|
||||
var screen = DisplayServer.GetPrimaryScreen();
|
||||
var screenSize = DisplayServer.ScreenGetSize(screen);
|
||||
var position = (screenSize - newSize) / 2;
|
||||
DisplayServer.WindowSetPosition(position);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 音频设置管理
|
||||
|
||||
#### 音量变化平滑过渡
|
||||
|
||||
```csharp
|
||||
public class AudioManager : Node
|
||||
{
|
||||
private Tween _volumeTween;
|
||||
|
||||
public async Task SmoothVolumeTransition(float targetMasterVolume, float duration = 1.0f)
|
||||
{
|
||||
var currentVolume = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex("Master"));
|
||||
var currentLinear = Mathf.DbToLinear(currentVolume);
|
||||
|
||||
_volumeTween?.Kill();
|
||||
_volumeTween = CreateTween();
|
||||
|
||||
_volumeTween.TweenMethod(
|
||||
new Callable(this, nameof(SetMasterVolume)),
|
||||
currentLinear,
|
||||
targetMasterVolume,
|
||||
duration
|
||||
);
|
||||
}
|
||||
|
||||
private async void SetMasterVolume(float linearVolume)
|
||||
{
|
||||
var settings = new AudioSettings { MasterVolume = linearVolume };
|
||||
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
||||
await audioSettings.ApplyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用自定义总线映射的平滑过渡
|
||||
public class CustomAudioManager : Node
|
||||
{
|
||||
private Tween _volumeTween;
|
||||
private AudioBusMap _customBusMap;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_customBusMap = new AudioBusMap
|
||||
{
|
||||
Master = "Master_Bus",
|
||||
Bgm = "Background_Music",
|
||||
Sfx = "Sound_Effects"
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SmoothVolumeTransition(float targetMasterVolume, float duration = 1.0f)
|
||||
{
|
||||
var settings = new AudioSettings { MasterVolume = targetMasterVolume };
|
||||
var currentVolume = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex(_customBusMap.Master));
|
||||
var currentLinear = Mathf.DbToLinear(currentVolume);
|
||||
|
||||
_volumeTween?.Kill();
|
||||
_volumeTween = CreateTween();
|
||||
|
||||
_volumeTween.TweenMethod(
|
||||
new Callable(this, nameof(SetMasterVolume)),
|
||||
currentLinear,
|
||||
targetMasterVolume,
|
||||
duration
|
||||
);
|
||||
}
|
||||
|
||||
private async void SetMasterVolume(float linearVolume)
|
||||
{
|
||||
var audioSettingsData = new AudioSettings { MasterVolume = linearVolume };
|
||||
var audioSettings = new GodotAudioSettings(audioSettingsData, _customBusMap);
|
||||
await audioSettings.ApplyAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 音频设置验证
|
||||
|
||||
```csharp
|
||||
public static class AudioSettingsValidator
|
||||
{
|
||||
public static bool ValidateBusNames(AudioBusMap busMap)
|
||||
{
|
||||
var masterIndex = AudioServer.GetBusIndex(busMap.Master);
|
||||
var bgmIndex = AudioServer.GetBusIndex(busMap.Bgm);
|
||||
var sfxIndex = AudioServer.GetBusIndex(busMap.Sfx);
|
||||
|
||||
return masterIndex >= 0 && bgmIndex >= 0 && sfxIndex >= 0;
|
||||
}
|
||||
|
||||
public static void LogMissingBuses(AudioBusMap busMap)
|
||||
{
|
||||
if (AudioServer.GetBusIndex(busMap.Master) < 0)
|
||||
GD.PrintErr($"Master bus not found: {busMap.Master}");
|
||||
|
||||
if (AudioServer.GetBusIndex(busMap.Bgm) < 0)
|
||||
GD.PrintErr($"BGM bus not found: {busMap.Bgm}");
|
||||
|
||||
if (AudioServer.GetBusIndex(busMap.Sfx) < 0)
|
||||
GD.PrintErr($"SFX bus not found: {busMap.Sfx}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 图形设置管理
|
||||
|
||||
#### 分辨率变更安全检查
|
||||
|
||||
```csharp
|
||||
public static class DisplayValidator
|
||||
{
|
||||
public static bool IsResolutionSupported(int width, int height)
|
||||
{
|
||||
var screen = DisplayServer.GetPrimaryScreen();
|
||||
var screenSize = DisplayServer.ScreenGetSize(screen);
|
||||
|
||||
return width <= screenSize.x && height <= screenSize.y;
|
||||
}
|
||||
|
||||
public static (int width, int height) GetMaxSafeResolution()
|
||||
{
|
||||
var screen = DisplayServer.GetPrimaryScreen();
|
||||
var screenSize = DisplayServer.ScreenGetSize(screen);
|
||||
|
||||
return ((int)screenSize.x, (int)screenSize.y);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 图形设置持久化
|
||||
|
||||
```csharp
|
||||
public class GraphicsSettingsManager : Node
|
||||
{
|
||||
private const string SettingsKey = "graphics_settings";
|
||||
private GodotGraphicsSettings _settings;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
var storage = new GodotFileStorage(new JsonSerializer());
|
||||
|
||||
try
|
||||
{
|
||||
_settings = storage.Read<GodotGraphicsSettings>(SettingsKey);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
_settings = new GodotGraphicsSettings
|
||||
{
|
||||
ResolutionWidth = 1920,
|
||||
ResolutionHeight = 1080,
|
||||
Fullscreen = false
|
||||
};
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
var storage = new GodotFileStorage(new JsonSerializer());
|
||||
storage.Write(SettingsKey, _settings);
|
||||
}
|
||||
|
||||
public async Task ApplyAndSave()
|
||||
{
|
||||
await _settings.ApplyAsync();
|
||||
SaveSettings();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 1. 音频设置应用
|
||||
|
||||
- 音频总线查找是 O(1) 操作
|
||||
- 音量转换计算开销很小
|
||||
- 建议批量应用多个音量设置
|
||||
|
||||
### 2. 图形设置应用
|
||||
|
||||
- 窗口操作需要系统调用,相对较慢
|
||||
- 分辨率变更可能触发窗口重建
|
||||
- 避免频繁切换显示模式
|
||||
|
||||
### 3. 设置持久化
|
||||
|
||||
- 使用异步文件 I/O
|
||||
- 考虑设置变更防抖机制
|
||||
- 压缩设置文件以减少 I/O 开销
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 音频总线未找到
|
||||
|
||||
```
|
||||
错误:Audio bus not found: CustomBGM
|
||||
解决:确保在 Godot 项目中创建了对应的音频总线
|
||||
```
|
||||
|
||||
#### 2. 分辨率设置无效
|
||||
|
||||
```
|
||||
错误:分辨率无法设置到指定值
|
||||
解决:检查分辨率是否超出显示器支持范围
|
||||
```
|
||||
|
||||
#### 3. 全屏模式问题
|
||||
|
||||
```
|
||||
错误:全屏切换失败
|
||||
解决:检查是否在调试器中运行,某些全屏模式在调试时可能不可用
|
||||
```
|
||||
|
||||
### 调试技巧
|
||||
|
||||
#### 音频调试
|
||||
|
||||
```csharp
|
||||
// 打印所有音频总线信息
|
||||
for (int i = 0; i < AudioServer.GetBusCount(); i++)
|
||||
{
|
||||
var name = AudioServer.GetBusName(i);
|
||||
var volume = AudioServer.GetBusVolumeDb(i);
|
||||
GD.Print($"Bus {i}: {name} ({volume} dB)");
|
||||
}
|
||||
```
|
||||
|
||||
#### 图形调试
|
||||
|
||||
```csharp
|
||||
// 打印当前显示信息
|
||||
var screen = DisplayServer.GetPrimaryScreen();
|
||||
var screenSize = DisplayServer.ScreenGetSize(screen);
|
||||
var windowSize = DisplayServer.WindowGetSize();
|
||||
var windowPos = DisplayServer.WindowGetPosition();
|
||||
var windowMode = DisplayServer.WindowGetMode();
|
||||
|
||||
GD.Print($"Screen: {screenSize}");
|
||||
GD.Print($"Window: {windowSize} at {windowPos}");
|
||||
GD.Print($"Mode: {windowMode}");
|
||||
```
|
||||
1. [Godot 运行时集成](./index.md)
|
||||
2. [Game 设置系统](../game/setting.md)
|
||||
3. [Godot 存储系统](./storage.md)
|
||||
4. [Godot 集成教程](../tutorials/godot-integration.md)
|
||||
|
||||
@ -1,275 +1,138 @@
|
||||
# 存储模块 (Storage Module)
|
||||
---
|
||||
title: Godot 存储系统
|
||||
description: 以当前 GFramework.Godot 源码与 CoreGrid 接线为准,说明 GodotFileStorage 的职责、路径边界和最小接入方式。
|
||||
---
|
||||
|
||||
## 概述
|
||||
# Godot 存储系统
|
||||
|
||||
存储模块是 GFramework.Godot 的核心存储实现,专门为 Godot 引擎设计的文件存储系统。该模块支持 Godot 的虚拟路径系统(如
|
||||
`res://` 和 `user://`),并提供了按键级别的细粒度锁机制来保证线程安全。
|
||||
`GFramework.Godot` 在存储这一层提供的核心入口只有 `GodotFileStorage`。
|
||||
|
||||
## 核心类
|
||||
它实现 `GFramework.Game` 侧统一的 `IStorage` 契约,负责把序列化后的读写、目录列举和路径处理接到 Godot 的
|
||||
`res://`、`user://` 和普通文件系统路径上,而不是另外提供一套独立的“Godot 专属存档框架”。
|
||||
|
||||
### GodotFileStorage
|
||||
## 当前公开入口
|
||||
|
||||
Godot 特化的文件存储实现,实现了 `IStorage` 接口。
|
||||
### `GodotFileStorage`
|
||||
|
||||
**主要特性:**
|
||||
`GodotFileStorage` 的当前职责比较集中:
|
||||
|
||||
- ✅ Godot 虚拟路径支持(`res://`, `user://`)
|
||||
- ✅ 线程安全(按键级别的细粒度锁)
|
||||
- ✅ 同步/异步读写操作
|
||||
- ✅ 自动创建目录结构
|
||||
- ❌ 删除操作(Delete 方法未实现)
|
||||
- 对外暴露 `IStorage` 约定的 `Read`、`Write`、`Exists`、`Delete`、目录列举与目录创建能力
|
||||
- 识别并保留 Godot 虚拟路径:`res://`、`user://`
|
||||
- 对普通文件系统路径做段级清理,并拒绝包含 `..` 的非法 key
|
||||
- 使用 `IAsyncKeyLockManager` 对“绝对路径 / Godot 路径”做按 key 细粒度串行化
|
||||
|
||||
## 功能特性
|
||||
构造函数默认会在未注入锁管理器时创建内部 `AsyncKeyLockManager`。这意味着:
|
||||
|
||||
### 路径处理
|
||||
- 同一个 `GodotFileStorage` 实例内,不同文件可以并发访问
|
||||
- 同一个目标路径的读写 / 删除会被串行化
|
||||
- 锁作用域只限当前进程内的当前实例,不是跨进程文件锁
|
||||
|
||||
该存储系统支持三种路径类型:
|
||||
## 路径语义
|
||||
|
||||
#### 1. Godot 资源路径 (`res://`)
|
||||
### `res://`
|
||||
|
||||
- **用途**:存储游戏资源文件
|
||||
- **特点**:只读,包含在游戏构建中
|
||||
- **示例**:`res://config/game_settings.json`
|
||||
`res://` 更适合作为只读资源或配置源目录。
|
||||
|
||||
#### 2. Godot 用户数据路径 (`user://`)
|
||||
当前实现不会阻止你把它传给 `ReadAsync`、`ExistsAsync` 之类的方法,但在导出后的 Godot 项目里,`res://`
|
||||
通常不应被当作用户可写存储根目录。存档、设置和运行时缓存应优先落到 `user://`。
|
||||
|
||||
- **用途**:存储用户数据、存档、配置等
|
||||
- **特点**:可读写,游戏可访问的用户目录
|
||||
- **示例**:`user://saves/save_001.dat`
|
||||
### `user://`
|
||||
|
||||
#### 3. 普通文件系统路径
|
||||
`user://` 是当前推荐的可写路径:
|
||||
|
||||
- **用途**:存储临时文件或调试数据
|
||||
- **特点**:完整的文件系统访问
|
||||
- **示例**:`C:/Games/MyGame/logs/debug.log`
|
||||
- 用户设置
|
||||
- 存档
|
||||
- 运行时缓存
|
||||
- 导出后仍需要读写的 JSON / YAML / 二进制数据
|
||||
|
||||
### 路径验证与清理
|
||||
如果调用 `ListDirectoriesAsync()` 或 `ListFilesAsync()` 时传入空字符串,当前实现会默认从 `user://` 根开始列举。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[输入路径] --> B{包含 ".." ?}
|
||||
B -->|是| C[抛出异常]
|
||||
B -->|否| D{是 Godot 路径?}
|
||||
D -->|是| E[直接使用]
|
||||
D -->|否| F[清理路径段]
|
||||
F --> G[替换无效字符]
|
||||
G --> H[创建目录结构]
|
||||
H --> I[返回绝对路径]
|
||||
C --> J[结束]
|
||||
E --> J
|
||||
I --> J
|
||||
```
|
||||
### 普通文件系统路径
|
||||
|
||||
### 线程安全机制
|
||||
当 key 不是 Godot 路径时,`GodotFileStorage` 会:
|
||||
|
||||
每个文件路径都有独立的锁对象,确保:
|
||||
1. 把 `\` 统一成 `/`
|
||||
2. 拒绝包含 `..` 的 key
|
||||
3. 按路径段清理非法文件名字符
|
||||
4. 在写入或建目录前自动补父目录
|
||||
|
||||
1. **细粒度锁** - 不同文件可以并发访问
|
||||
2. **避免死锁** - 锁的获取顺序一致
|
||||
3. **高性能** - 减少锁竞争
|
||||
这条路径更适合测试、桌面工具链或显式指定外部目录的宿主环境,不建议在项目业务层自己重新拼装一套路径清理逻辑。
|
||||
|
||||
## API 接口
|
||||
## 最小接入路径
|
||||
|
||||
### IStorage 接口
|
||||
当前消费者 `ai-libs/CoreGrid` 的接法是先注册同一个序列化器和存储实例,再让设置仓库、存档仓库等上层组件复用它:
|
||||
|
||||
```csharp
|
||||
public interface IStorage
|
||||
{
|
||||
// 读取操作
|
||||
T Read<T>(string key);
|
||||
T Read<T>(string key, T defaultValue);
|
||||
Task<T> ReadAsync<T>(string key);
|
||||
|
||||
// 写入操作
|
||||
void Write<T>(string key, T value);
|
||||
Task WriteAsync<T>(string key, T value);
|
||||
|
||||
// 检查存在性
|
||||
bool Exists(string key);
|
||||
Task<bool> ExistsAsync(string key);
|
||||
|
||||
// 删除操作(未实现)
|
||||
void Delete(string key);
|
||||
}
|
||||
using GFramework.Core.Abstractions.Serializer;
|
||||
using GFramework.Core.Abstractions.Storage;
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Storage;
|
||||
using GFramework.Godot.Storage;
|
||||
using Godot;
|
||||
|
||||
var jsonSerializer = new JsonSerializer();
|
||||
architecture.RegisterUtility<ISerializer>(jsonSerializer);
|
||||
|
||||
var storage = new GodotFileStorage(jsonSerializer);
|
||||
architecture.RegisterUtility<IStorage>(storage);
|
||||
|
||||
architecture.RegisterUtility(new UnifiedSettingsDataRepository(
|
||||
storage,
|
||||
jsonSerializer,
|
||||
new DataRepositoryOptions
|
||||
{
|
||||
BasePath = ProjectSettings.GetSetting("application/config/save/setting_path").AsString(),
|
||||
AutoBackup = true
|
||||
}));
|
||||
|
||||
architecture.RegisterUtility<ISaveRepository<GameSaveData>>(new SaveRepository<GameSaveData>(
|
||||
storage,
|
||||
new SaveConfiguration
|
||||
{
|
||||
SaveRoot = ProjectSettings.GetSetting("application/config/save/save_path").AsString(),
|
||||
SaveSlotPrefix = ProjectSettings.GetSetting("application/config/save/save_slot_prefix").AsString(),
|
||||
SaveFileName = ProjectSettings.GetSetting("application/config/save/save_file_name").AsString()
|
||||
}));
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
这里的分工是:
|
||||
|
||||
### 基本使用
|
||||
- `GodotFileStorage` 负责底层 key -> 文件读写
|
||||
- `UnifiedSettingsDataRepository` 负责设置节聚合与持久化
|
||||
- `SaveRepository<TSaveData>` 负责存档结构和保存槽位语义
|
||||
|
||||
```csharp
|
||||
// 创建存储实例(需要序列化器)
|
||||
var serializer = new JsonSerializer(); // 或其他序列化器
|
||||
var storage = new GodotFileStorage(serializer);
|
||||
不要把 `GodotFileStorage` 本身写成“设置系统”或“存档系统”的 owner。
|
||||
|
||||
// 写入用户数据
|
||||
var userData = new UserData
|
||||
{
|
||||
PlayerName = "Alice",
|
||||
Level = 5,
|
||||
Score = 1000
|
||||
};
|
||||
storage.Write("user://player.dat", userData);
|
||||
## 什么时候应该改看别的入口
|
||||
|
||||
// 读取用户数据
|
||||
var loadedData = storage.Read<UserData>("user://player.dat");
|
||||
Console.WriteLine($"Player: {loadedData.PlayerName}, Level: {loadedData.Level}");
|
||||
```
|
||||
### 配置 YAML / schema 文本加载
|
||||
|
||||
### 异步操作
|
||||
如果你的目标是读取 `res://` 下的 YAML 配置,并在导出态同步到运行时缓存,请优先看
|
||||
[`../game/config-system.md`](../game/config-system.md) 里的 `GodotYamlConfigLoader` 接法。
|
||||
|
||||
```csharp
|
||||
// 异步写入游戏配置
|
||||
var config = new GameConfig
|
||||
{
|
||||
Resolution = "1920x1080",
|
||||
Fullscreen = true,
|
||||
Volume = 0.8f
|
||||
};
|
||||
await storage.WriteAsync("user://config.json", config);
|
||||
这类场景的重点不是通用键值存储,而是:
|
||||
|
||||
// 异步读取配置
|
||||
var loadedConfig = await storage.ReadAsync<GameConfig>("user://config.json");
|
||||
```
|
||||
- `res://` 与 `user://` 缓存切换
|
||||
- 生成器表元数据
|
||||
- 热重载可用性边界
|
||||
|
||||
### 不同路径类型使用
|
||||
### 通用存储契约
|
||||
|
||||
```csharp
|
||||
// 读取游戏资源(只读)
|
||||
var levelData = storage.Read<LevelData>("res://levels/level_001.json");
|
||||
如果你想先理解 `IStorage`、`ScopedStorage`、`FileStorage` 和统一数据仓库的宿主无关语义,应先看
|
||||
[`../game/storage.md`](../game/storage.md)。
|
||||
|
||||
// 存储用户存档
|
||||
var saveData = new SaveData { /* ... */ };
|
||||
storage.Write("user://saves/slot_001.dat", saveData);
|
||||
本页只补 Godot 宿主差异,不重复维护一份跨宿主 API 手册。
|
||||
|
||||
// 存储调试信息(普通路径)
|
||||
var debugLog = new DebugLog { /* ... */ };
|
||||
storage.Write("logs/debug_" + DateTime.UtcNow.Ticks + ".json", debugLog);
|
||||
```
|
||||
## 当前边界
|
||||
|
||||
### 存在性检查
|
||||
- 同步 `Read` / `Write` / `Delete` / `Exists` 只是对异步方法的阻塞包装;在带同步上下文的宿主里,优先使用异步 API
|
||||
- `GodotFileStorage` 不负责文件扩展名约定、作用域前缀或保存槽位策略,这些属于上层 repository / scoped storage
|
||||
- 路径安全只覆盖当前 key 的格式校验与路径段清理,不代替业务层的目录规划
|
||||
- 当前实现支持目录列举与目录创建,但没有额外的“监视目录变化”或“自动迁移目录结构”能力
|
||||
|
||||
```csharp
|
||||
// 检查文件是否存在
|
||||
if (storage.Exists("user://settings.json"))
|
||||
{
|
||||
var settings = storage.Read<AppSettings>("user://settings.json");
|
||||
// 使用设置...
|
||||
}
|
||||
else
|
||||
{
|
||||
// 使用默认设置
|
||||
var defaultSettings = new AppSettings();
|
||||
storage.Write("user://settings.json", defaultSettings);
|
||||
}
|
||||
```
|
||||
## 继续阅读
|
||||
|
||||
### 带默认值的读取
|
||||
|
||||
```csharp
|
||||
// 尝试读取,如果文件不存在则返回默认值
|
||||
var settings = storage.Read("user://user_prefs.json", new UserPrefs
|
||||
{
|
||||
Language = "en",
|
||||
Volume = 1.0f,
|
||||
Difficulty = 1
|
||||
});
|
||||
```
|
||||
|
||||
## 路径扩展
|
||||
|
||||
该模块使用了路径扩展方法:
|
||||
|
||||
```csharp
|
||||
public static class GodotPathExtensions
|
||||
{
|
||||
public static bool IsUserPath(this string path);
|
||||
public static bool IsResPath(this string path);
|
||||
public static bool IsGodotPath(this string path);
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
string path1 = "user://save.dat";
|
||||
string path2 = "res://config.json";
|
||||
string path3 = "C:/temp/file.txt";
|
||||
|
||||
Console.WriteLine(path1.IsGodotPath()); // true
|
||||
Console.WriteLine(path1.IsUserPath()); // true
|
||||
Console.WriteLine(path2.IsResPath()); // true
|
||||
Console.WriteLine(path3.IsGodotPath()); // false
|
||||
```
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 1. 锁机制
|
||||
|
||||
- 每个文件路径独立锁,减少锁竞争
|
||||
- 读写操作串行化,避免数据损坏
|
||||
|
||||
### 2. 文件访问
|
||||
|
||||
- Godot 虚拟路径使用 `FileAccess` API
|
||||
- 普通路径使用标准 .NET 文件 I/O
|
||||
- 自动创建目录结构
|
||||
|
||||
### 3. 内存使用
|
||||
|
||||
- 锁对象使用 `ConcurrentDictionary` 管理
|
||||
- 锁对象按需创建,避免内存泄漏
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见异常
|
||||
|
||||
1. **ArgumentException** - 路径参数无效
|
||||
- 空路径
|
||||
- 包含 ".." 的路径
|
||||
- 无效的存储键
|
||||
|
||||
2. **FileNotFoundException** - 文件不存在
|
||||
- 读取不存在的文件时抛出
|
||||
|
||||
3. **IOException** - 文件操作失败
|
||||
- 写入权限不足
|
||||
- 磁盘空间不足
|
||||
|
||||
### 错误处理示例
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
var data = storage.Read<UserData>("user://save.dat");
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
Console.WriteLine("存档文件不存在,创建新的存档");
|
||||
var newSave = new UserData();
|
||||
storage.Write("user://save.dat", newSave);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"读取存档失败: {ex.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **路径选择**
|
||||
- 游戏资源使用 `res://`
|
||||
- 用户数据使用 `user://`
|
||||
- 调试/临时文件使用普通路径
|
||||
|
||||
2. **异常处理**
|
||||
- 总是处理 `FileNotFoundException`
|
||||
- 使用带默认值的 `Read` 重载方法
|
||||
|
||||
3. **性能优化**
|
||||
- 批量读写时使用异步方法
|
||||
- 避免频繁的小文件操作
|
||||
|
||||
4. **序列化器选择**
|
||||
- JSON:人类可读,调试友好
|
||||
- 二进制:性能更好,文件更小
|
||||
1. [Godot 运行时集成](./index.md)
|
||||
2. [Game 存储系统](../game/storage.md)
|
||||
3. [Game 配置系统](../game/config-system.md)
|
||||
4. [Godot 集成教程](../tutorials/godot-integration.md)
|
||||
|
||||
@ -67,6 +67,10 @@ RegisterCqrsHandlersFromAssemblies(
|
||||
]);
|
||||
```
|
||||
|
||||
文档示例统一用 marker 类型承载程序集引用。框架本身不要求固定目录或固定命名,但团队实践里可以把这类空 marker
|
||||
集中放在每个业务程序集自己的 `Application/Markers` 或等价目录,并采用 `InventoryCqrsMarker` 这类能直接看出来源
|
||||
的名字,避免多人协作时拿无关业务类型充当程序集定位锚点。
|
||||
|
||||
## 运行时如何消费生成结果
|
||||
|
||||
`Cqrs` runtime 当前的注册顺序是:
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
---
|
||||
title: Source Generators
|
||||
description: 按模块梳理 GFramework 当前发布的源码生成器包、运行时归属与推荐选包入口。
|
||||
---
|
||||
|
||||
# Source Generators
|
||||
|
||||
`Source Generators` 栏目对应 `GFramework` 当前按模块拆分发布的编译期工具链。
|
||||
@ -23,7 +28,7 @@ GFramework 当前发布的生成器包是:
|
||||
- 选择 `GeWuYou.GFramework.Game.SourceGenerators`
|
||||
- 想让 CQRS handler registry 在编译期生成,缩小运行时反射扫描范围:
|
||||
- 选择 `GeWuYou.GFramework.Cqrs.SourceGenerators`
|
||||
- 想在 Godot 项目里生成 AutoLoad / Input Action 入口,或减少节点与信号样板代码:
|
||||
- 想在 Godot 项目里生成 AutoLoad / Input Action 入口、节点 / 信号样板,或补齐 Scene/UI 包装与导出集合注册辅助:
|
||||
- 选择 `GeWuYou.GFramework.Godot.SourceGenerators`
|
||||
|
||||
## 与运行时的关系
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user