Compare commits

...

27 Commits

Author SHA1 Message Date
gewuyou
7cfdd2cf21
Merge pull request #297 from GeWuYou/fix/analyzer-warning-reduction-batch
Fix/analyzer warning reduction batch
2026-04-27 16:59:57 +08:00
gewuyou
1753778cae fix(game): 修复同步加载阶段的取消透传
- 修复 YAML 同步反序列化与构表阶段的取消处理,避免已取消会话被包装为配置加载失败
- 补充私有同步路径的回归测试,覆盖反序列化与构表阶段的 OperationCanceledException 透传语义
2026-04-27 16:50:44 +08:00
gewuyou
953a03b937 fix(game-tests): 修复取消读取测试的异步警告
- 修复取消读取回归测试的外层 async Task 签名,消除没有 await 的编译警告
- 优化异步异常断言,继续验证取消流程会保留 OperationCanceledException 语义
2026-04-27 16:06:51 +08:00
gewuyou
686647c06b fix(game): 修复 YAML 热重载取消语义
- 修复 ReadYamlAsync 在取消时错误包装异常的问题,并对齐 IntegerTryParseDelegate 的可空性签名

- 更新 Ioc 与 Query 测试辅助类型的 XML 文档,并让 IPrioritizedService 复用 IMixedService 的 Name 契约

- 补充 YamlConfigLoader 取消语义回归测试并同步 analyzer warning reduction 跟踪
2026-04-27 14:26:30 +08:00
gewuyou
99ccc28697
Merge pull request #296 from GeWuYou/docs/sdk-update-documentation
Docs/sdk update documentation
2026-04-27 13:22:54 +08:00
gewuyou
4a5e1e74a6 docs(pr-review): 收口当前文档审查意见
- 更新 Game 与 SourceGenerators README 的公开入口命名和重复链接

- 优化 Godot 教程与扩展页的 reader-facing 措辞

- 补充 PR #296 的治理跟踪与验证记录
2026-04-27 12:49:34 +08:00
gewuyou
a9904a35be fix(warning-reduction): 清理配置与测试切片告警
- 修复 YamlConfigLoader 的超长方法、依赖比较与热重载同步原语告警

- 拆分 MicrosoftDiContainerTests 与 AbstractAsyncQueryTests 的辅助类型文件以消除 MA0048

- 更新 analyzer warning reduction 跟踪文档并记录 non-incremental 构建基线变化
2026-04-27 11:57:49 +08:00
gewuyou
86cfaa7122 test(architectures): 拆分优先级测试辅助类型文件
- 拆分 PriorityServiceTests 末尾的测试接口与辅助类型到独立文件
- 保留原有命名空间、可见性与优先级测试语义
- 补充新文件的 XML 注释并清理原测试文件的无关 using
2026-04-27 11:17:19 +08:00
gewuyou
b6a9fefda9
Merge pull request #295 from GeWuYou/fix/analyzer-warning-reduction-batch
Fix/analyzer warning reduction batch
2026-04-27 10:53:34 +08:00
gewuyou
067d72fada fix(tooling): 收口PR评审遗留nitpick
- 修复 PR review 脚本对 failed-test 额外列表格的解析容错

- 清理 AsyncExtensionsTests 中多余的等待并保留参数名断言

- 补充脚本回归测试并同步 analyzer-warning-reduction 恢复点
2026-04-27 10:22:45 +08:00
gewuyou
1c87272f6b fix(tooling): 补全PR测试报告解析并修复并发测试
- 更新 gframework-pr-review 脚本以提取 CTRF 测试摘要和失败用例详情

- 修复 SettingsModelTests 在 NET9+ 下错误使用 Monitor 持锁的并发测试语义

- 同步 analyzer-warning-reduction 的 active todo 与 trace 真值
2026-04-27 09:53:12 +08:00
gewuyou
1f560635a8 fix(analyzer): 收口PR评审遗留问题
- 修复 AsyncExtensionsTests 中 ArgumentException 的 ParamName 传递与断言契约

- 更新 analyzer warning reduction 的 active todo 与 trace 真值

- 归档 RP073-RP078 的历史恢复文档以收紧当前入口
2026-04-27 09:19:13 +08:00
gewuyou
5778782df0 docs(godot): 收口旧文档口吻与采用说明
- 更新 Godot 与教程细页的 reader-facing 采用说明

- 修复旧文档、ai-libs 与内部术语在公开页面中的暴露

- 更新文档治理恢复点并记录接近阈值的停止状态
2026-04-27 09:02:48 +08:00
gewuyou
979db3b5a5 docs(reader-facing): 统一站内入口与公开术语
- 更新入口页的 reader-facing 骨架,统一起步路线、阅读顺序与站内导航
- 收口公开 README 与 Godot 页面中的内部口吻、文件名式表述和术语噪音
- 移除 docs/zh-CN 中残留的 GitHub README 外链,并同步刷新文档治理恢复状态
2026-04-27 08:55:18 +08:00
gewuyou
72ebd266d3 docs(analyzer): 同步第三轮警告清理恢复点
- 更新 analyzer-warning-reduction 跟踪文档的第三轮结果与最新 stop-condition 指标

- 记录 Core.Tests 批次验证结果与默认收口建议
2026-04-27 08:15:36 +08:00
gewuyou
e19e60ea1a fix(core-tests): 修复 AsyncKeyLockManagerTests 的 MA0004 warning
- 修复 Task.Run 内 await using 的异步释放上下文捕获 warning
- 保持 AsyncKeyLockManager 并发测试语义与可读性不变
2026-04-27 08:09:35 +08:00
gewuyou
650618b5ab fix(core-tests): 修复 PauseStackManagerTests 锁分析器警告
- 修复 PauseStackManagerTests 并发测试中的锁声明,针对 net9 以上使用专用 Lock。

- 保持 net8.0 回退到 object 锁,确保多目标兼容且测试行为不变。
2026-04-27 08:09:26 +08:00
gewuyou
946cdbb9d2 fix(analyzer): 收口第二轮游戏侧警告清理
- 修复 SettingsModel 与 GameConfigBootstrap 的残留 MA0158 专用锁警告

- 更新 analyzer-warning-reduction 恢复点与第二轮构建验证结果
2026-04-27 08:06:27 +08:00
gewuyou
9ce634ed1c refactor(game): 拆分配置热重载启动流程
- 重构 GameConfigBootstrap 的热重载启动流程,提取状态准备、结果提交与失败回滚辅助方法

- 保持现有锁保护、异常路径与监听句柄释放语义不变,消除 StartHotReload 的 MA0051 warning
2026-04-27 08:00:17 +08:00
gewuyou
9deafac234 fix(game): 清理路由与 UI 交互配置的 analyzer warning
- 修复 RouterBase 中路由键比较的 MA0006,显式使用 Ordinal 字符串比较

- 修复 UiInteractionProfiles 中位掩码判定的 MA0099,改为与显式枚举值比较
2026-04-27 07:59:27 +08:00
gewuyou
c106e53a74 fix(game): 修复 SettingsModel 的 MA0004 警告
- 修复 SettingsModel 中异步仓储加载与保存流程缺少 ConfigureAwait(false) 的 analyzer 警告

- 保持设置模型初始化、保存、应用阶段的生命周期与事件触发语义不变
2026-04-27 07:58:22 +08:00
gewuyou
fb0a55f435 fix(analyzer): 收口首轮并行警告清理
- 修复 Core 与 Cqrs 中资源、日志、配置缓存的 MA0158 专用锁警告

- 修复 SaveRepository 与 SceneRouterBase 的残留分析器警告

- 更新 analyzer-warning-reduction 跟踪文档与最新构建验证结果
2026-04-27 07:54:43 +08:00
gewuyou
5befaf707b docs(ai-plan): 更新文档治理恢复状态
- 更新文档治理 tracking 的 branch diff 指标与当前工作树状态
- 补充本轮提交后的下一步恢复建议
2026-04-27 07:44:10 +08:00
gewuyou
8f2d95910e fix(core): 迁移 MA0158 专用锁实现
- 迁移 Events、Property、State 与 Coroutine 中 7 个类型的监视器字段到 NET9_0_OR_GREATER 专用 Lock 模式
- 保持 net8.0 的 object 回退路径以兼容多目标构建
- 更新 BindableProperty 的同步注释以匹配新的多目标同步原语
2026-04-27 07:42:20 +08:00
gewuyou
1454c81a5b docs(adoption): 收口安装入口与公开文案
- 更新安装页的选包矩阵、推荐组合与 Godot 基线说明
- 收口公开 README 的 XML 阅读入口表述,移除治理式计数与日期字段
- 调整配置系统与基础教程入口的 reader-facing 文案,并同步更新恢复文档
2026-04-27 07:42:10 +08:00
gewuyou
e3eec5452c fix(game): 修复数据仓库与场景路由分析器警告
- 修复数据仓库异步存储调用的 ConfigureAwait(false) 使用,消除目标 MA0004 警告

- 更新 UnifiedSettingsDataRepository 的字符串键字典 comparer 为 StringComparer.Ordinal,消除目标 MA0002 警告

- 保留场景切换流程在当前上下文继续执行,并显式使用 ConfigureAwait(true) 说明上下文约束
2026-04-27 07:41:10 +08:00
gewuyou
7e13752bb1 fix(game): 修复 UiRouterBase 分析器警告
- 修复 UiRouterBase 中缺少参数名的 ArgumentException 调用

- 更新 UI 键比较与层级字典为 Ordinal 语义,消除字符串与比较器相关警告
2026-04-27 07:40:46 +08:00
120 changed files with 2294 additions and 1001 deletions

View File

@ -62,6 +62,7 @@ The script should produce:
- Pre-merge failed checks, if present
- Latest MegaLinter status and any detailed issues posted by `github-actions[bot]`
- Test summary, including failed-test signals when present
- Detailed failed-test rows from GitHub Test Reporter / CTRF comments when the PR comment includes `Name` / `Failure Message` content
- CLI support for writing full JSON to a file and printing only narrowed text sections to stdout
- Parse warnings only when both the primary API source and the intended fallback signal are unavailable

View File

@ -257,6 +257,11 @@ def strip_markdown_links(text: str) -> str:
return re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
def strip_markdown_images(text: str) -> str:
"""Drop Markdown image syntax while keeping surrounding text readable."""
return re.sub(r"!\[[^\]]*\]\([^)]+\)", "", text)
def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str | None:
"""Extract text between a start marker and the earliest matching end marker."""
start = text.find(start_marker)
@ -486,43 +491,198 @@ def parse_megalinter_comment(comment_body: str) -> dict[str, Any]:
return report
def clean_markdown_table_cell(text: str) -> str:
"""Normalize a Markdown table cell for structured parsing."""
cleaned = strip_markdown_images(strip_markdown_links(html.unescape(text)))
cleaned = cleaned.replace("\xa0", " ")
cleaned = cleaned.replace("**", "").replace("*", "").replace("`", "")
return collapse_whitespace(cleaned)
def parse_int_from_text(text: str) -> int | None:
"""Extract the first integer value from text."""
match = re.search(r"\d+", text)
return int(match.group(0)) if match else None
def parse_duration_from_text(text: str) -> str:
"""Extract a duration token from text when present."""
match = re.search(r"\d+(?:\.\d+)?(?:ms|s|m|h)", text)
if match is not None:
return match.group(0)
return collapse_whitespace(text)
def parse_markdown_table(table_text: str) -> tuple[list[str], list[list[str]]]:
"""Parse a Markdown table into header cells and row cells."""
lines = [line.strip() for line in table_text.splitlines() if line.strip().startswith("|")]
if len(lines) < 2:
return [], []
headers = [clean_markdown_table_cell(cell) for cell in lines[0].strip("|").split("|")]
rows: list[list[str]] = []
for line in lines[2:]:
cells = [clean_markdown_table_cell(cell) for cell in line.strip("|").split("|")]
if cells:
rows.append(cells)
return headers, rows
def extract_markdown_table_after_heading(block: str, heading: str) -> tuple[list[str], list[list[str]]]:
"""Extract the first Markdown table that appears after a heading."""
section = extract_section(block, heading, ["\n### ", "\n#### ", "\n<details>", "\n<table>", "\n<sub>"])
if section is None:
return [], []
table_match = re.search(r"(\|.*\|\n\|[-| :]+\|\n(?:\|.*\|\n?)*)", section, re.S)
if table_match is None:
return [], []
return parse_markdown_table(table_match.group(1))
def normalize_stat_header(header: str) -> str:
"""Normalize a human-readable stats header into a stable machine key."""
ascii_only = re.sub(r"[^A-Za-z]+", "", header).lower()
aliases = {
"tests": "tests",
"passed": "passed",
"failed": "failed",
"skipped": "skipped",
"pending": "pending",
"other": "other",
"flaky": "flaky",
"duration": "duration",
}
return aliases.get(ascii_only, ascii_only)
def parse_stats_table(headers: list[str], rows: list[list[str]]) -> dict[str, Any]:
"""Convert a parsed Markdown stats table into the report stats shape."""
if not headers or not rows:
return {}
first_row = rows[0]
stats: dict[str, Any] = {}
for header, value in zip(headers, first_row):
key = normalize_stat_header(header)
if not key:
continue
if key == "duration":
stats[key] = parse_duration_from_text(value)
continue
parsed_value = parse_int_from_text(value)
if parsed_value is not None:
stats[key] = parsed_value
return stats
def normalize_failure_message(text: str) -> str:
"""Normalize a failed-test message while preserving the meaningful lines."""
cleaned = html.unescape(text)
cleaned = re.sub(r"(?i)<br\s*/?>", "\n", cleaned)
cleaned = re.sub(r"</?(?:p|div|tbody|thead|tr|td|th|table)>", "\n", cleaned)
cleaned = re.sub(r"<[^>]+>", " ", cleaned)
lines = [collapse_whitespace(line) for line in cleaned.splitlines()]
meaningful_lines = [line for line in lines if line]
return "\n".join(meaningful_lines)
def parse_failed_test_summary_list(block: str) -> list[str]:
"""Parse the compact failed-tests summary list from CTRF details blocks."""
failed_tests_section = re.search(
r"<details><summary><strong>\s*Failed Tests.*?</summary>(?P<body>.*?)</details>",
block,
re.S,
)
if failed_tests_section is None:
return []
summary_body = strip_markdown_links(strip_markdown_images(html.unescape(failed_tests_section.group("body"))))
failed_tests: list[str] = []
for raw_line in summary_body.splitlines():
line = collapse_whitespace(raw_line)
if not line:
continue
if "arrow-right" in raw_line:
parts = [part.strip() for part in line.split("arrow-right") if part.strip()]
candidate = parts[-1] if parts else line
elif ">" in line:
candidate = line.split(">")[-1].strip()
else:
candidate = line
if candidate:
failed_tests.append(candidate)
return failed_tests
def parse_failed_test_details(block: str) -> list[dict[str, str]]:
"""Parse the detailed failed-test HTML table from GitHub Test Reporter comments."""
details: list[dict[str, str]] = []
table_section = re.search(
r"### ❌ \*\*Some tests failed!\*\*.*?<tbody>(?P<body>.*?)</tbody>",
block,
re.S,
)
if table_section is None:
return details
row_pattern = re.compile(
r"<tr>\s*<td>(?P<name>.*?)</td>\s*<td>(?P<message>.*?)</td>(?:\s*<td>.*?</td>)*\s*</tr>",
re.S,
)
# Test Reporter tables may grow extra columns over time; only the first two are required here.
for row_match in row_pattern.finditer(table_section.group("body")):
name_cell = row_match.group("name")
message_cell = row_match.group("message")
name = collapse_whitespace(strip_tags(html.unescape(name_cell))).lstrip("").strip()
failure_message = normalize_failure_message(message_cell)
if name:
details.append(
{
"name": name,
"failure_message": failure_message,
}
)
return details
def parse_test_report(block: str) -> dict[str, Any]:
"""Parse a CTRF or GitHub test-reporter comment block."""
report: dict[str, Any] = {
"raw": block.strip(),
"stats": {},
"failed_tests": [],
"failed_test_details": [],
"has_failed_tests": False,
}
summary_row_match = re.search(
r"\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|"
r"\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?([^\|]+?)\*?\*?\s*\|",
block,
)
if summary_row_match is not None:
report["stats"] = {
"tests": int(summary_row_match.group(1)),
"passed": int(summary_row_match.group(2)),
"failed": int(summary_row_match.group(3)),
"skipped": int(summary_row_match.group(4)),
"other": int(summary_row_match.group(5)),
"flaky": int(summary_row_match.group(6)),
"duration": summary_row_match.group(7).strip(),
}
summary_headers, summary_rows = extract_markdown_table_after_heading(block, "### Summary")
report["stats"] = parse_stats_table(summary_headers, summary_rows)
failed_tests_section = extract_section(
block,
"### Failed Tests",
["### Slowest Tests", "### Insights", "<sub>", "[Github Test Reporter]"],
)
if failed_tests_section:
lines = [line.strip("- ").strip() for line in failed_tests_section.splitlines()[1:] if line.strip()]
report["failed_tests"] = lines
report["has_failed_tests"] = True
elif "No failed tests in this run." in block or "All tests passed!" in block:
report["failed_tests"] = []
report["has_failed_tests"] = False
if not report["stats"]:
build_headers, build_rows = extract_markdown_table_after_heading(block, "### build-and-test:")
report["stats"] = parse_stats_table(build_headers, build_rows)
failed_test_details = parse_failed_test_details(block)
failed_test_names = parse_failed_test_summary_list(block)
if not failed_test_names and failed_test_details:
failed_test_names = [detail["name"] for detail in failed_test_details]
report["failed_tests"] = failed_test_names
report["failed_test_details"] = failed_test_details
failed_count = int(report["stats"].get("failed", 0) or 0)
report["has_failed_tests"] = bool(failed_test_names or failed_test_details or failed_count > 0)
return report
@ -1103,8 +1263,17 @@ def format_text(
lines.append(f"- Report {index}: no structured test stats parsed")
if report["has_failed_tests"]:
for failed_test in report["failed_tests"]:
lines.append(f" Failed test: {truncate_text(failed_test, max_description_length)}")
failed_test_details = report.get("failed_test_details", [])
if failed_test_details:
for failed_test_detail in failed_test_details:
lines.append(f" Failed test: {truncate_text(failed_test_detail['name'], max_description_length)}")
lines.append(
" Failure: "
f"{truncate_text(failed_test_detail['failure_message'].replace(chr(10), ' | '), max_description_length)}"
)
else:
for failed_test in report["failed_tests"]:
lines.append(f" Failed test: {truncate_text(failed_test, max_description_length)}")
else:
lines.append(" Failed tests: none reported")

View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Regression tests for the GFramework PR review fetch helper."""
from __future__ import annotations
import importlib.util
from pathlib import Path
import unittest
SCRIPT_PATH = Path(__file__).with_name("fetch_current_pr_review.py")
MODULE_SPEC = importlib.util.spec_from_file_location("fetch_current_pr_review", SCRIPT_PATH)
if MODULE_SPEC is None or MODULE_SPEC.loader is None:
raise RuntimeError(f"Unable to load module from {SCRIPT_PATH}.")
MODULE = importlib.util.module_from_spec(MODULE_SPEC)
MODULE_SPEC.loader.exec_module(MODULE)
class ParseFailedTestDetailsTests(unittest.TestCase):
"""Cover failed-test table parsing edge cases for CTRF comments."""
def test_parse_failed_test_details_ignores_trailing_columns(self) -> None:
"""Extra columns should not prevent extracting the name and failure message."""
block = """
### ❌ **Some tests failed!**
<table>
<tbody>
<tr>
<td> RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache</td>
<td><pre>Expected: False\nBut was: True</pre></td>
<td>failed</td>
<td>35.3s</td>
</tr>
</tbody>
</table>
"""
details = MODULE.parse_failed_test_details(block)
self.assertEqual(
details,
[
{
"name": "RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache",
"failure_message": "Expected: False\nBut was: True",
}
],
)
if __name__ == "__main__":
unittest.main()

View File

@ -36,18 +36,17 @@
## XML 阅读入口
截至 `2026-04-22`,下面这份目录视图可以帮助你快速定位 `GFramework.Core.Abstractions` 的类型级 XML 文档入口;当前契约目录族的类型声明都已带
XML 注释。更细的契约约束与交互语义,适合在阅读具体接口和成员时继续结合源码确认。
下面这份目录视图可以帮助你快速定位 `GFramework.Core.Abstractions` 的代表类型。更细的契约约束与交互语义,适合在阅读具体接口和成员时继续结合源码确认。
| 类型族 | 基线状态 | 代表类型 |
| 类型族 | 代表类型 | 阅读重点 |
| --- | --- | --- |
| `Architectures/` `Lifecycle/` `Registries/` | `20/20` 个类型声明已带 XML 注释 | `IArchitecture`、`IArchitectureContext``IServiceModule``KeyValueRegistryBase<TKey, TValue>` |
| `Command/` `Query/` `Cqrs/` | `10/10` 个类型声明已带 XML 注释 | `ICommandExecutor`、`IAsyncQueryExecutor``ICqrsRuntime` |
| `Events/` `Property/` `State/` `StateManagement/` | `25/25` 个类型声明已带 XML 注释 | `IEventBus`、`IBindableProperty<T>``IStateMachine``IStore<TState>` |
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `17/17` 个类型声明已带 XML 注释 | `IYieldInstruction`、`ITimeProvider``IPauseStackManager``IAsyncKeyLockManager` |
| `Resource/` `Pool/` `Logging/` `Localization/` | `27/27` 个类型声明已带 XML 注释 | `IResourceManager`、`IObjectPoolSystem``ILogger``ILocalizationManager` |
| `Configuration/` `Environment/` `Data/` `Serializer/` `Storage/` `Versioning/` | `7/7` 个类型声明已带 XML 注释 | `IConfigurationManager`、`IEnvironment``ILoadableFrom<T>``ISerializer``IStorage` |
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` `Enums/` `Properties/` | `19/19` 个类型声明已带 XML 注释 | `IPrioritized`、`IController``IModel``ISystem``IContextUtility``ArchitecturePhase` |
| `Architectures/` `Lifecycle/` `Registries/` | `IArchitecture`、`IArchitectureContext``IServiceModule``KeyValueRegistryBase<TKey, TValue>` | 看架构、上下文、模块装配与注册表基类边界 |
| `Command/` `Query/` `Cqrs/` | `ICommandExecutor`、`IAsyncQueryExecutor``ICqrsRuntime` | 看命令、查询与新请求模型的调用入口 |
| `Events/` `Property/` `State/` `StateManagement/` | `IEventBus`、`IBindableProperty<T>``IStateMachine``IStore<TState>` | 看事件分发、可绑定状态与 store 契约 |
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `IYieldInstruction`、`ITimeProvider``IPauseStackManager``IAsyncKeyLockManager` | 看协程、时间源、暂停栈与并发协调能力 |
| `Resource/` `Pool/` `Logging/` `Localization/` | `IResourceManager`、`IObjectPoolSystem``ILogger``ILocalizationManager` | 看资源、对象池、日志与本地化服务角色 |
| `Configuration/` `Environment/` `Data/` `Serializer/` `Storage/` `Versioning/` | `IConfigurationManager`、`IEnvironment``ILoadableFrom<T>``ISerializer``IStorage` | 看配置、环境、数据装载、序列化与存储边界 |
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` `Enums/` `Properties/` | `IPrioritized`、`IController``IModel``ISystem``IContextUtility``ArchitecturePhase` | 看组件角色、优先级和值对象约定 |
完整接入说明与阅读顺序见 [Core 抽象层说明](../docs/zh-CN/abstractions/core-abstractions.md)。

View File

@ -176,6 +176,6 @@ public sealed class TrackingPipelineBehavior<TRequest, TResponse> : IPipelineBeh
CancellationToken cancellationToken)
{
InvocationCount++;
return await next(message, cancellationToken);
return await next(message, cancellationToken).ConfigureAwait(false);
}
}

View File

@ -0,0 +1,10 @@
using GFramework.Core.Abstractions.Systems;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 定义用于混合优先级排序测试的系统契约。
/// </summary>
public interface IMixedTestSystem : ISystem
{
}

View File

@ -0,0 +1,10 @@
using GFramework.Core.Abstractions.Model;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 定义用于优先级排序测试的模型契约。
/// </summary>
public interface IPriorityTestModel : IModel
{
}

View File

@ -0,0 +1,10 @@
using GFramework.Core.Abstractions.Systems;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 定义用于优先级排序测试的系统契约。
/// </summary>
public interface IPriorityTestSystem : ISystem
{
}

View File

@ -0,0 +1,10 @@
using GFramework.Core.Abstractions.Utility;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 定义用于优先级排序测试的工具契约。
/// </summary>
public interface IPriorityTestUtility : IUtility
{
}

View File

@ -0,0 +1,21 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示显式声明负优先级的混合测试系统。
/// </summary>
public class MixedTestSystemNegativePriority : AbstractSystem, IMixedTestSystem, IPrioritized
{
/// <summary>
/// 获取当前测试系统的排序优先级。
/// </summary>
public int Priority => -10;
/// <summary>
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -0,0 +1,21 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示显式声明正优先级的混合测试系统。
/// </summary>
public class MixedTestSystemWithPriority : AbstractSystem, IMixedTestSystem, IPrioritized
{
/// <summary>
/// 获取当前测试系统的排序优先级。
/// </summary>
public int Priority => 10;
/// <summary>
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -0,0 +1,16 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示未声明优先级、依赖默认排序值的混合测试系统。
/// </summary>
public class MixedTestSystemWithoutPriority : AbstractSystem, IMixedTestSystem
{
/// <summary>
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -1,12 +1,7 @@
using System.Reflection;
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Core.Model;
namespace GFramework.Core.Tests.Architectures;
@ -120,129 +115,3 @@ public class PriorityServiceTests
Assert.That(systems[2], Is.InstanceOf<MixedTestSystemWithPriority>()); // 10
}
}
#region Test Interfaces
public interface IPriorityTestSystem : ISystem
{
}
public interface IPriorityTestModel : IModel
{
}
public interface IPriorityTestUtility : IUtility
{
}
public interface IMixedTestSystem : ISystem
{
}
#endregion
#region Test Systems
public class PriorityTestSystemA : AbstractSystem, IPriorityTestSystem, IPrioritized
{
public int Priority => 10;
protected override void OnInit()
{
}
}
public class PriorityTestSystemB : AbstractSystem, IPriorityTestSystem, IPrioritized
{
public int Priority => 20;
protected override void OnInit()
{
}
}
public class PriorityTestSystemC : AbstractSystem, IPriorityTestSystem, IPrioritized
{
public int Priority => 30;
protected override void OnInit()
{
}
}
public class MixedTestSystemWithPriority : AbstractSystem, IMixedTestSystem, IPrioritized
{
public int Priority => 10;
protected override void OnInit()
{
}
}
public class MixedTestSystemWithoutPriority : AbstractSystem, IMixedTestSystem
{
protected override void OnInit()
{
}
}
public class MixedTestSystemNegativePriority : AbstractSystem, IMixedTestSystem, IPrioritized
{
public int Priority => -10;
protected override void OnInit()
{
}
}
#endregion
#region Test Models
public class PriorityTestModelA : AbstractModel, IPriorityTestModel, IPrioritized
{
public int Priority => 10;
protected override void OnInit()
{
}
}
public class PriorityTestModelB : AbstractModel, IPriorityTestModel, IPrioritized
{
public int Priority => 20;
protected override void OnInit()
{
}
}
public class PriorityTestModelC : AbstractModel, IPriorityTestModel, IPrioritized
{
public int Priority => 30;
protected override void OnInit()
{
}
}
#endregion
#region Test Utilities
public class PriorityTestUtilityA : IPriorityTestUtility, IPrioritized
{
public int Priority => 10;
}
public class PriorityTestUtilityB : IPriorityTestUtility, IPrioritized
{
public int Priority => 20;
}
public class PriorityTestUtilityC : IPriorityTestUtility, IPrioritized
{
public int Priority => 30;
}
#endregion

View File

@ -0,0 +1,22 @@
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Model;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示优先级为 10 的测试模型。
/// </summary>
public class PriorityTestModelA : AbstractModel, IPriorityTestModel, IPrioritized
{
/// <summary>
/// 获取当前测试模型的排序优先级。
/// </summary>
public int Priority => 10;
/// <summary>
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -0,0 +1,22 @@
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Model;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示优先级为 20 的测试模型。
/// </summary>
public class PriorityTestModelB : AbstractModel, IPriorityTestModel, IPrioritized
{
/// <summary>
/// 获取当前测试模型的排序优先级。
/// </summary>
public int Priority => 20;
/// <summary>
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -0,0 +1,22 @@
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Model;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示优先级为 30 的测试模型。
/// </summary>
public class PriorityTestModelC : AbstractModel, IPriorityTestModel, IPrioritized
{
/// <summary>
/// 获取当前测试模型的排序优先级。
/// </summary>
public int Priority => 30;
/// <summary>
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -0,0 +1,21 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示优先级为 10 的测试系统。
/// </summary>
public class PriorityTestSystemA : AbstractSystem, IPriorityTestSystem, IPrioritized
{
/// <summary>
/// 获取当前测试系统的排序优先级。
/// </summary>
public int Priority => 10;
/// <summary>
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -0,0 +1,21 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示优先级为 20 的测试系统。
/// </summary>
public class PriorityTestSystemB : AbstractSystem, IPriorityTestSystem, IPrioritized
{
/// <summary>
/// 获取当前测试系统的排序优先级。
/// </summary>
public int Priority => 20;
/// <summary>
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -0,0 +1,21 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示优先级为 30 的测试系统。
/// </summary>
public class PriorityTestSystemC : AbstractSystem, IPriorityTestSystem, IPrioritized
{
/// <summary>
/// 获取当前测试系统的排序优先级。
/// </summary>
public int Priority => 30;
/// <summary>
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -0,0 +1,14 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示优先级为 10 的测试工具。
/// </summary>
public class PriorityTestUtilityA : IPriorityTestUtility, IPrioritized
{
/// <summary>
/// 获取当前测试工具的排序优先级。
/// </summary>
public int Priority => 10;
}

View File

@ -0,0 +1,14 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示优先级为 20 的测试工具。
/// </summary>
public class PriorityTestUtilityB : IPriorityTestUtility, IPrioritized
{
/// <summary>
/// 获取当前测试工具的排序优先级。
/// </summary>
public int Priority => 20;
}

View File

@ -0,0 +1,14 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 表示优先级为 30 的测试工具。
/// </summary>
public class PriorityTestUtilityC : IPriorityTestUtility, IPrioritized
{
/// <summary>
/// 获取当前测试工具的排序优先级。
/// </summary>
public int Priority => 30;
}

View File

@ -47,9 +47,11 @@ public sealed class AsyncKeyLockManagerTests
var index = i;
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync("same-key").ConfigureAwait(false);
executionOrder.Add(index);
await Task.Delay(10).ConfigureAwait(false);
await using ((await manager.AcquireLockAsync("same-key").ConfigureAwait(false)).ConfigureAwait(false))
{
executionOrder.Add(index);
await Task.Delay(10).ConfigureAwait(false);
}
}));
}
@ -75,11 +77,13 @@ public sealed class AsyncKeyLockManagerTests
var key = $"key-{i}";
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync(key).ConfigureAwait(false);
var current = Interlocked.Increment(ref concurrentCount);
maxConcurrent = Math.Max(maxConcurrent, current);
await Task.Delay(50).ConfigureAwait(false);
Interlocked.Decrement(ref concurrentCount);
await using ((await manager.AcquireLockAsync(key).ConfigureAwait(false)).ConfigureAwait(false))
{
var current = Interlocked.Increment(ref concurrentCount);
maxConcurrent = Math.Max(maxConcurrent, current);
await Task.Delay(50).ConfigureAwait(false);
Interlocked.Decrement(ref concurrentCount);
}
}));
}
@ -117,8 +121,10 @@ public sealed class AsyncKeyLockManagerTests
var key = $"key-{i % 10}";
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync(key).ConfigureAwait(false);
await Task.Delay(1).ConfigureAwait(false);
await using ((await manager.AcquireLockAsync(key).ConfigureAwait(false)).ConfigureAwait(false))
{
await Task.Delay(1).ConfigureAwait(false);
}
}));
}
@ -139,10 +145,12 @@ public sealed class AsyncKeyLockManagerTests
{
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync("same-key").ConfigureAwait(false);
var temp = counter;
await Task.Delay(1).ConfigureAwait(false);
counter = temp + 1;
await using ((await manager.AcquireLockAsync("same-key").ConfigureAwait(false)).ConfigureAwait(false))
{
var temp = counter;
await Task.Delay(1).ConfigureAwait(false);
counter = temp + 1;
}
}));
}
@ -295,8 +303,10 @@ public sealed class AsyncKeyLockManagerTests
{
for (var j = 0; j < 10; j++)
{
await using var handle = await manager.AcquireLockAsync($"key-{j % 5}").ConfigureAwait(false);
await Task.Delay(10).ConfigureAwait(false);
await using ((await manager.AcquireLockAsync($"key-{j % 5}").ConfigureAwait(false)).ConfigureAwait(false))
{
await Task.Delay(10).ConfigureAwait(false);
}
}
}));
}

View File

@ -225,23 +225,31 @@ public class AsyncExtensionsTests
/// 测试WithRetry方法遵守ShouldRetry谓词
/// </summary>
[Test]
public async Task WithRetry_Should_Respect_ShouldRetry_Predicate()
public void WithRetry_Should_Respect_ShouldRetry_Predicate()
{
static Task<int> ThrowShouldNotRetry(string parameterName)
{
throw new ArgumentException("Should not retry", parameterName);
}
// Arrange
var attemptCount = 0;
Func<Task<int>> taskFactory = () =>
{
attemptCount++;
throw new ArgumentException("Should not retry");
return ThrowShouldNotRetry(nameof(taskFactory));
};
// Act & Assert
Assert.ThrowsAsync<AggregateException>(() =>
var exception = Assert.ThrowsAsync<AggregateException>(() =>
taskFactory.WithRetryAsync(3, TimeSpan.FromMilliseconds(10),
ex => ex is not ArgumentException));
await Task.Delay(50).ConfigureAwait(false); // 等待任务完成
Assert.That(attemptCount, Is.EqualTo(1)); // 不应该重试
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.InnerExceptions, Has.Count.EqualTo(1));
Assert.That(exception.InnerExceptions[0], Is.TypeOf<ArgumentException>());
Assert.That(((ArgumentException)exception.InnerExceptions[0]).ParamName, Is.EqualTo(nameof(taskFactory)));
}
/// <summary>

View File

@ -0,0 +1,8 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 同时实现多个别名接口的测试服务。
/// </summary>
public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService
{
}

View File

@ -0,0 +1,12 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 混合服务接口(用于测试优先级和非优先级混合)
/// </summary>
public interface IMixedService
{
/// <summary>
/// 获取或设置服务名称。
/// </summary>
string? Name { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 主服务别名接口。
/// </summary>
public interface IPrimaryAliasService : ISharedAliasService;

View File

@ -0,0 +1,10 @@
using GFramework.Core.Abstractions.Bases;
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 优先级服务接口
/// </summary>
public interface IPrioritizedService : IPrioritized, IMixedService
{
}

View File

@ -0,0 +1,6 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 次级兼容别名接口。
/// </summary>
public interface ISecondaryAliasService : ISharedAliasService;

View File

@ -0,0 +1,6 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 服务接口定义
/// </summary>
public interface IService;

View File

@ -0,0 +1,6 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 用于验证未冻结查询路径中的服务别名去重行为。
/// </summary>
public interface ISharedAliasService;

View File

@ -1,5 +1,4 @@
using System.Reflection;
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
@ -734,74 +733,3 @@ public class MicrosoftDiContainerTests
Assert.That(((IPrioritizedService)services[1]).Priority, Is.EqualTo(30));
}
}
/// <summary>
/// 服务接口定义
/// </summary>
public interface IService;
/// <summary>
/// 测试服务类,实现 IService 接口
/// </summary>
public sealed class TestService : IService
{
/// <summary>
/// 获取或设置优先级
/// </summary>
public int Priority { get; set; }
}
/// <summary>
/// 优先级服务接口
/// </summary>
public interface IPrioritizedService : IPrioritized
{
string? Name { get; set; }
}
/// <summary>
/// 混合服务接口(用于测试优先级和非优先级混合)
/// </summary>
public interface IMixedService
{
string? Name { get; set; }
}
/// <summary>
/// 用于验证未冻结查询路径中的服务别名去重行为。
/// </summary>
public interface ISharedAliasService;
/// <summary>
/// 主服务别名接口。
/// </summary>
public interface IPrimaryAliasService : ISharedAliasService;
/// <summary>
/// 次级兼容别名接口。
/// </summary>
public interface ISecondaryAliasService : ISharedAliasService;
/// <summary>
/// 同时实现多个别名接口的测试服务。
/// </summary>
public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService
{
}
/// <summary>
/// 实现优先级的服务
/// </summary>
public sealed class PrioritizedService : IPrioritizedService, IMixedService
{
public int Priority { get; set; }
public string? Name { get; set; }
}
/// <summary>
/// 不实现优先级的服务
/// </summary>
public sealed class NonPrioritizedService : IMixedService
{
public string? Name { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 不实现优先级的服务
/// </summary>
public sealed class NonPrioritizedService : IMixedService
{
/// <summary>
/// 获取或设置服务名称
/// </summary>
public string? Name { get; set; }
}

View File

@ -0,0 +1,17 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 实现优先级的服务
/// </summary>
public sealed class PrioritizedService : IPrioritizedService
{
/// <summary>
/// 获取或设置优先级
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 获取或设置服务名称
/// </summary>
public string? Name { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace GFramework.Core.Tests.Ioc;
/// <summary>
/// 测试服务类,实现 IService 接口
/// </summary>
public sealed class TestService : IService
{
/// <summary>
/// 获取或设置优先级
/// </summary>
public int Priority { get; set; }
}

View File

@ -431,7 +431,11 @@ public class PauseStackManagerTests
{
var tasks = new List<Task>();
var tokens = new List<PauseToken>();
#if NET9_0_OR_GREATER
var lockObj = new System.Threading.Lock();
#else
var lockObj = new object();
#endif
for (int i = 0; i < 100; i++)
{

View File

@ -6,7 +6,6 @@ using GFramework.Core.Environment;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Core.Query;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Tests.Query;
@ -236,179 +235,3 @@ public class AbstractAsyncQueryTests
Assert.That(result2, Is.EqualTo(40));
}
}
/// <summary>
/// 测试用异步查询输入类V2
/// </summary>
public sealed class TestAsyncQueryInputV2 : IQueryInput
{
/// <summary>
/// 获取或设置值
/// </summary>
public int Value { get; init; }
}
/// <summary>
/// 整数类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果将输入值乘以2</returns>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult(input.Value * 2);
}
}
/// <summary>
/// 字符串类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncStringQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, string>
{
/// <summary>
/// 初始化TestAsyncStringQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncStringQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>格式化的字符串结果</returns>
protected override Task<string> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult($"Value: {input.Value * 2}");
}
}
/// <summary>
/// 复杂对象类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncComplexQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, TestAsyncQueryResultV2>
{
/// <summary>
/// 初始化TestAsyncComplexQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncComplexQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>复杂对象查询结果</returns>
protected override Task<TestAsyncQueryResultV2> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
var result = new TestAsyncQueryResultV2
{
Value = input.Value * 2,
DoubleValue = input.Value * 3
};
return Task.FromResult(result);
}
}
/// <summary>
/// 测试用异步查询类(抛出异常)
/// </summary>
public sealed class TestAsyncQueryWithExceptionV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryWithExceptionV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryWithExceptionV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 执行异步查询操作并抛出异常
/// </summary>
/// <param name="input">查询输入参数</param>
/// <exception cref="InvalidOperationException">总是抛出异常</exception>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
throw new InvalidOperationException("Test exception");
}
}
/// <summary>
/// 测试用异步查询子类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncQueryChildV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryChildV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryChildV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现子类实现乘以3
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果将输入值乘以3</returns>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult(input.Value * 3);
}
}
/// <summary>
/// 测试用复杂查询结果类V2
/// </summary>
public sealed class TestAsyncQueryResultV2
{
/// <summary>
/// 获取或设置值
/// </summary>
public int Value { get; init; }
/// <summary>
/// 获取或设置双倍值
/// </summary>
public int DoubleValue { get; init; }
}

View File

@ -0,0 +1,38 @@
using GFramework.Core.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 复杂对象类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncComplexQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, TestAsyncQueryResultV2>
{
/// <summary>
/// 初始化TestAsyncComplexQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncComplexQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>复杂对象查询结果</returns>
protected override Task<TestAsyncQueryResultV2> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
var result = new TestAsyncQueryResultV2
{
Value = input.Value * 2,
DoubleValue = input.Value * 3
};
return Task.FromResult(result);
}
}

View File

@ -0,0 +1,33 @@
using GFramework.Core.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 测试用异步查询子类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncQueryChildV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryChildV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryChildV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现子类实现乘以3
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果将输入值乘以3</returns>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult(input.Value * 3);
}
}

View File

@ -0,0 +1,14 @@
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 测试用异步查询输入类V2
/// </summary>
public sealed class TestAsyncQueryInputV2 : IQueryInput
{
/// <summary>
/// 获取或设置值
/// </summary>
public int Value { get; init; }
}

View File

@ -0,0 +1,17 @@
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 测试用复杂查询结果类V2
/// </summary>
public sealed class TestAsyncQueryResultV2
{
/// <summary>
/// 获取或设置值
/// </summary>
public int Value { get; init; }
/// <summary>
/// 获取或设置双倍值
/// </summary>
public int DoubleValue { get; init; }
}

View File

@ -0,0 +1,33 @@
using GFramework.Core.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 整数类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果将输入值乘以2</returns>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult(input.Value * 2);
}
}

View File

@ -0,0 +1,28 @@
using GFramework.Core.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 测试用异步查询类(抛出异常)
/// </summary>
public sealed class TestAsyncQueryWithExceptionV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
{
/// <summary>
/// 初始化TestAsyncQueryWithExceptionV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncQueryWithExceptionV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 执行异步查询操作并抛出异常
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>返回一个不会正常完成的 <see cref="Task{TResult}" />,因为该方法始终抛出异常。</returns>
/// <exception cref="InvalidOperationException">总是抛出异常</exception>
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
{
throw new InvalidOperationException("Test exception");
}
}

View File

@ -0,0 +1,33 @@
using GFramework.Core.Query;
namespace GFramework.Core.Tests.Query;
/// <summary>
/// 字符串类型测试异步查询类V4继承AbstractAsyncQuery
/// </summary>
public sealed class TestAsyncStringQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, string>
{
/// <summary>
/// 初始化TestAsyncStringQueryV4的新实例
/// </summary>
/// <param name="input">查询输入参数</param>
public TestAsyncStringQueryV4(TestAsyncQueryInputV2 input) : base(input)
{
}
/// <summary>
/// 获取查询是否已执行
/// </summary>
public bool Executed { get; private set; }
/// <summary>
/// 执行异步查询操作的具体实现
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>格式化的字符串结果</returns>
protected override Task<string> OnDoAsync(TestAsyncQueryInputV2 input)
{
Executed = true;
return Task.FromResult($"Value: {input.Value * 2}");
}
}

View File

@ -40,7 +40,13 @@ public class ConfigurationManager : IConfigurationManager
/// <summary>
/// 用于保护监听器列表的锁
/// </summary>
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _watcherLock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _watcherLock = new();
#endif
/// <summary>
/// 配置监听器字典(线程安全)

View File

@ -12,7 +12,13 @@ internal sealed class CoroutineStatistics : ICoroutineStatistics
{
private readonly Dictionary<CoroutinePriority, int> _countByPriority = new();
private readonly Dictionary<string, int> _countByTag = new(StringComparer.Ordinal);
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
private int _activeCount;
private double _maxExecutionTimeMs;
private int _pausedCount;

View File

@ -10,7 +10,13 @@ namespace GFramework.Core.Events;
public sealed class EventStatistics : IEventStatistics
{
private readonly Dictionary<string, int> _listenerCountByType = new(StringComparer.Ordinal);
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
private readonly Dictionary<string, long> _publishCountByType = new(StringComparer.Ordinal);
private long _totalFailed;
private long _totalHandled;

View File

@ -10,7 +10,13 @@ namespace GFramework.Core.Events;
public sealed class FilterableEvent<T>
{
private readonly List<IEventFilter<T>> _filters = new();
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
private readonly EventStatistics? _statistics;
private Action<T>? _onEvent;
@ -152,4 +158,4 @@ public sealed class FilterableEvent<T>
var count = _onEvent?.GetInvocationList().Length ?? 0;
_statistics.UpdateListenerCount(typeof(T).Name, count);
}
}
}

View File

@ -21,7 +21,13 @@ public class PriorityEvent<T> : IEvent
/// <summary>
/// 保护处理器集合的并发访问
/// </summary>
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _syncRoot = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _syncRoot = new();
#endif
/// <summary>
/// 标记事件是否已被处理(用于 UntilHandled 传播模式)
@ -326,4 +332,4 @@ public class PriorityEvent<T> : IEvent
public Action<EventContext<T>> Handler { get; } = handler;
public int Priority { get; } = priority;
}
}
}

View File

@ -10,7 +10,13 @@ namespace GFramework.Core.Events;
/// <typeparam name="T">事件数据类型</typeparam>
public sealed class WeakEvent<T>
{
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
private readonly EventStatistics? _statistics;
private readonly List<WeakReference<Action<T>>> _weakHandlers = new();
@ -151,4 +157,4 @@ public sealed class WeakEvent<T>
var count = _weakHandlers.Count(wr => wr.TryGetTarget(out _));
_statistics.UpdateListenerCount(typeof(T).Name, count);
}
}
}

View File

@ -13,7 +13,13 @@ public sealed class FileAppender : ILogAppender, IDisposable
private readonly string _filePath;
private readonly ILogFilter? _filter;
private readonly ILogFormatter _formatter;
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
private bool _disposed;
private StreamWriter? _writer;
@ -114,4 +120,4 @@ public sealed class FileAppender : ILogAppender, IDisposable
AutoFlush = true
};
}
}
}

View File

@ -14,7 +14,13 @@ public sealed class RollingFileAppender : ILogAppender, IDisposable
private readonly string _baseFilePath;
private readonly ILogFilter? _filter;
private readonly ILogFormatter _formatter;
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
private readonly int _maxFileCount;
private readonly long _maxFileSize;
private long _currentSize;
@ -205,4 +211,4 @@ public sealed class RollingFileAppender : ILogAppender, IDisposable
// 获取当前文件大小
_currentSize = File.Exists(_baseFilePath) ? new FileInfo(_baseFilePath).Length : 0;
}
}
}

View File

@ -91,7 +91,13 @@ public sealed class SamplingFilter : ILogFilter
/// </summary>
private sealed class SamplingState
{
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
private readonly ITimeProvider _timeProvider;
private long _count;
private long _lastAccessTicks;

View File

@ -12,9 +12,15 @@ namespace GFramework.Core.Property;
public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<T>
{
/// <summary>
/// 用于保护委托链和值访问的锁对象
/// 用于保护委托链和值访问的同步原语
/// </summary>
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
/// <summary>
/// 属性值变化事件回调委托,当属性值发生变化时被调用
@ -172,4 +178,4 @@ public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<
{
return Value?.ToString() ?? string.Empty;
}
}
}

View File

@ -14,7 +14,13 @@ internal sealed class ResourceCache
private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace.";
private readonly ConcurrentDictionary<string, ResourceCacheEntry> _cache = new(StringComparer.Ordinal);
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
/// <summary>
/// 获取已缓存资源的数量

View File

@ -11,7 +11,13 @@ namespace GFramework.Core.Resource;
/// <typeparam name="T">资源类型</typeparam>
internal sealed class ResourceHandle<T> : IResourceHandle<T> where T : class
{
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ResourceHandle<T>));
private readonly Action<string> _onDispose;
private bool _disposed;
@ -141,4 +147,4 @@ internal sealed class ResourceHandle<T> : IResourceHandle<T> where T : class
_logger.Error($"[ResourceHandle] Error disposing resource '{Path}': {ex.Message}");
}
}
}
}

View File

@ -18,7 +18,13 @@ public class ResourceManager : IResourceManager
private readonly ResourceCache _cache = new();
private readonly ConcurrentDictionary<Type, object> _loaders = new();
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _loadLock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _loadLock = new();
#endif
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ResourceManager));
private IResourceReleaseStrategy _releaseStrategy;

View File

@ -8,7 +8,13 @@ namespace GFramework.Core.State;
/// </summary>
public class StateMachine(int maxHistorySize = 10) : IStateMachine
{
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
private readonly HashSet<IState> _registeredStates = [];
private readonly Stack<IState> _stateHistory = new();

View File

@ -1,6 +1,6 @@
# GFramework.Cqrs.Abstractions
`GFramework.Cqrs.Abstractions` 提供 GFramework CQRS 的最小契约层。它只包含消息接口、处理器接口、运行时 seam 和管道契约,不包含默认 dispatcher、处理器扫描或任何 `GFramework.Core` 运行时实现。适合以下场景:
`GFramework.Cqrs.Abstractions` 提供 GFramework CQRS 的最小契约层。它只包含消息接口、处理器接口、运行时协作接口和管道契约,不包含默认 dispatcher、处理器扫描或任何 `GFramework.Core` 运行时实现。适合以下场景:
- 你的业务程序集只需要声明 Command、Query、Notification、Stream Request 或处理器接口。
- 你希望把消息契约放在更稳定的基础层,避免直接依赖默认 runtime 实现。
@ -43,7 +43,7 @@
- `Cqrs/IRequestHandler.cs`
- `Cqrs/INotificationHandler.cs`
- `Cqrs/IStreamRequestHandler.cs`
- 运行时 seam
- 运行时协作接口
- `Cqrs/ICqrsRuntime.cs`
- `Cqrs/ICqrsContext.cs`
- `Cqrs/ICqrsHandlerRegistrar.cs`
@ -93,7 +93,7 @@ public sealed class GetPlayerProfileHandler
- 只引用本包时,没有 `CommandBase<TInput, TResponse>``QueryBase<TInput, TResponse>``NotificationBase<TInput>` 等消息基类。
- 只引用本包时,没有 `AbstractCommandHandler``AbstractQueryHandler``AbstractNotificationHandler` 等处理器基类。
- `ICqrsContext` 当前是轻量 marker seam;默认 runtime 在需要向 `IContextAware` 处理器注入上下文时,仍要求传入的上下文同时实现 `IArchitectureContext`
- `ICqrsContext` 当前是轻量 marker 接口;默认 runtime 在需要向 `IContextAware` 处理器注入上下文时,仍要求传入的上下文同时实现 `IArchitectureContext`
## 文档入口

View File

@ -16,7 +16,13 @@ internal sealed class WeakKeyCache<TKey, TValue>
where TKey : class
where TValue : class
{
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _gate = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _gate = new();
#endif
private ConditionalWeakTable<TKey, TValue> _entries = new();
/// <summary>

View File

@ -37,11 +37,11 @@
下表汇总当前契约包的类型级 XML 文档入口,方便把 README、站内抽象页与源码阅读顺序对齐。
| 类型族 | 代表类型 | XML 状态 | 阅读重点 |
| --- | --- | --- | --- |
| 模块契约 | `IArchEcsModule` | 已覆盖 | 宿主循环如何统一驱动 ECS 更新 |
| 系统桥接契约 | `IArchSystemAdapter<T>` | 已覆盖 | 外部模块怎样只依赖更新接口而不绑定默认实现 |
| 配置对象 | `ArchOptions` | 已覆盖 | 跨程序集共享 ECS 配置边界 |
| 类型族 | 代表类型 | 阅读重点 |
| --- | --- | --- |
| 模块契约 | `IArchEcsModule` | 宿主循环如何统一驱动 ECS 更新 |
| 系统桥接契约 | `IArchSystemAdapter<T>` | 外部模块怎样只依赖更新接口而不绑定默认实现 |
| 配置对象 | `ArchOptions` | 跨程序集共享 ECS 配置边界 |
## 最小接入路径

View File

@ -1,6 +1,6 @@
# GFramework.Ecs.Arch
`GFramework.Ecs.Arch``GFramework` 当前 Arch ECS family 的默认运行时实现包。
`GFramework.Ecs.Arch``GFramework` 当前 Arch ECS 集成的默认运行时实现包。
它负责把 Arch `World`、GFramework 的服务模块生命周期,以及 `ArchSystemAdapter<T>` 系统桥接到同一条采用路径中。
如果你需要的只是共享契约,请改为依赖 `GFramework.Ecs.Arch.Abstractions`

View File

@ -25,8 +25,7 @@
- `FileStorage``ScopedStorage``JsonSerializer``SettingsModel<TRepository>``SaveRepository<TSaveData>``SceneRouterBase``UiRouterBase``YamlConfigLoader` 等都在实现这里的契约。
- 引擎适配包或项目代码
- `IUiFactory``ISceneFactory``IUiRoot``ISceneRoot`、资源注册表等通常由引擎适配层或游戏项目自己实现。
- 仓库内 `ai-libs/` 下的只读参考实现通常也是这样组织:页面 / 场景 factory、root、registry 在项目层,
运行时基类和契约来自 `GFramework.Game` 与本包。
- 常见做法也是这样组织:页面 / 场景 factory、root、registry 在项目层,运行时基类和契约来自 `GFramework.Game` 与本包。
## 子系统地图
@ -133,17 +132,16 @@ Scene 与 UI 路由共享这套基础约定。
## XML 阅读入口
下面这份目录视图汇总了 `2026-04-23` 可直接对照的 `GFramework.Game.Abstractions` 类型级 XML 文档入口:只统计公开 /
内部类型声明是否带 XML 注释,用来帮助你建立契约层阅读顺序;更细的参数、返回值、异常和生命周期说明,建议继续回到具体类型与成员确认。
下面这份目录视图用于帮助你建立 `GFramework.Game.Abstractions` 的契约层阅读顺序;更细的参数、返回值、异常和生命周期说明,建议继续回到具体类型与成员确认。
| 契约族 | 基线状态 | 代表类型 | 阅读重点 |
| --- | --- | --- | --- |
| `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` | 看公共路由上下文、存储角色、资源注册表与跨层共享枚举 |
| 契约族 | 代表类型 | 阅读重点 |
| --- | --- | --- |
| `Config/` | `IConfigLoader`、`IConfigRegistry``IConfigTable<TKey, TValue>``ConfigLoadException` | 看配置表注册、读取约定和失败诊断模型 |
| `Data/` | `IDataRepository`、`ISettingsDataRepository``ISaveRepository<TSaveData>``DataRepositoryOptions` | 看业务数据、设置持久化、槽位存档和版本迁移契约 |
| `Setting/` | `ISettingsData`、`ISettingsModel``ISettingsSystem``LocalizationSettings` | 看设置数据、应用语义、迁移接口和内置设置对象 |
| `Scene/` | `IScene`、`ISceneRouter``ISceneFactory``SceneTransitionEvent` | 看场景行为、路由、工厂 / root 边界与转场事件模型 |
| `UI/` | `IUiPage`、`IUiRouter``IUiFactory``UiInteractionProfile``UiTransitionHandlerOptions` | 看页面栈、层级 UI、输入动作与 UI 转场契约 |
| `Routing/` `Storage/` `Asset/` `Enums/` | `IRoute`、`IRouteContext``IFileStorage``IAssetRegistry<T>``UiLayer``SceneTransitionType` | 看公共路由上下文、存储角色、资源注册表与跨层共享枚举 |
## 最小接入路径
@ -210,9 +208,9 @@ public sealed class ContinueGameCommandHandler
也就是说,本包回答的是“项目各层如何约定”,`GFramework.Game` 回答的是“这些约定默认怎么跑起来”。
## `ai-libs/` 里的参考接入线索
## 典型分层方式
`ai-libs/` 下的只读参考实现对本包的使用方式,能比较清楚地说明它的职责边界:
典型项目对本包的使用方式,通常能清楚体现它的职责边界:
- 公共脚本广泛引用:
- `IUiRouter`

View File

@ -46,12 +46,12 @@ GameProject/
## XML 阅读入口
下面这份目录视图汇总了 `2026-04-23` 可直接对照的 `GFramework.Game.SourceGenerators` 类型级 XML 文档入口:只统计公开类型声明是否带 XML 注释,用来帮助你定位生成器入口;具体诊断消息、生成输出和兼容性语义仍建议回到源码与测试继续核对。
下面这份目录视图用于帮助你定位 `GFramework.Game.SourceGenerators`生成器入口;具体诊断消息、生成输出和兼容性语义仍建议回到源码与测试继续核对。
| 类型族 | 基线状态 | 代表类型 | 阅读重点 |
| --- | --- | --- | --- |
| `Config/` | `1/1` 个类型声明已带 XML 注释 | `SchemaConfigGenerator` | 看 schema 到配置类型 / 表包装 / 注册辅助代码的生成入口 |
| `Diagnostics/` | `1/1` 个类型声明已带 XML 注释 | `ConfigSchemaDiagnostics` | 看生成器会抛出的诊断类别与失败边界 |
| 阅读主题 | 代表类型 | 阅读重点 |
| --- | --- | --- |
| 配置生成入口 | `SchemaConfigGenerator` | 看 schema 到配置类型 / 表包装 / 注册辅助代码的生成入口 |
| 诊断与失败边界 | `ConfigSchemaDiagnostics` | 看生成器会抛出的诊断类别与失败边界 |
## 最小接入路径

View File

@ -1,7 +1,10 @@
using System.IO;
using System.Reflection;
using System.Threading;
using GFramework.Core.Abstractions.Events;
using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config;
using YamlDotNet.Serialization;
namespace GFramework.Game.Tests.Config;
@ -2784,6 +2787,130 @@ public class YamlConfigLoaderTests
});
}
/// <summary>
/// 验证底层文件读取在取消时会保留 <see cref="OperationCanceledException" />
/// 避免热重载把会话级取消误报为配置读取失败。
/// </summary>
[Test]
public void ReadYamlAsync_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
hp: 10
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
var registration = GetSingleYamlTableRegistration(loader);
var readYamlAsyncMethod = registration.GetType()
.GetMethod("ReadYamlAsync", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(readYamlAsyncMethod, Is.Not.Null);
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
// 通过反射直接命中注册项的文件读取路径,稳定回归本次取消语义修复。
var readTask = (Task<string>)readYamlAsyncMethod!.Invoke(
registration,
new object?[]
{
Path.Combine(_rootPath, "monster"),
Path.Combine(_rootPath, "monster", "slime.yaml"),
null,
cancellationTokenSource.Token
})!;
Assert.That(
async () => await readTask.ConfigureAwait(false),
Throws.InstanceOf<OperationCanceledException>());
}
/// <summary>
/// 验证同步反序列化阶段遇到已取消 token 时会直接透传 <see cref="OperationCanceledException" />
/// 避免把停止加载误报为 YAML 解析失败。
/// </summary>
[Test]
public void DeserializeValue_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested()
{
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
var registration = GetSingleYamlTableRegistration(loader);
var deserializeValueMethod = registration.GetType()
.GetMethod("DeserializeValue", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(deserializeValueMethod, Is.Not.Null);
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
var deserializer = new DeserializerBuilder().Build();
var exception = Assert.Throws<TargetInvocationException>(() =>
deserializeValueMethod!.Invoke(
registration,
new object?[]
{
deserializer,
Path.Combine(_rootPath, "monster"),
Path.Combine(_rootPath, "monster", "slime.yaml"),
null,
"""
id: 1
name: Slime
hp: 10
""",
cancellationTokenSource.Token
}));
// 反射调用同步私有方法时会把原始异常包装为 TargetInvocationException。
Assert.That(exception!.InnerException, Is.InstanceOf<OperationCanceledException>());
}
/// <summary>
/// 验证构建最终配置表阶段遇到已取消 token 时会继续透传 <see cref="OperationCanceledException" />
/// 避免热重载把提交前取消记录成构表失败。
/// </summary>
[Test]
public void BuildLoadResult_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested()
{
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
var registration = GetSingleYamlTableRegistration(loader);
var buildLoadResultMethod = registration.GetType()
.GetMethod("BuildLoadResult", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(buildLoadResultMethod, Is.Not.Null);
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
var exception = Assert.Throws<TargetInvocationException>(() =>
buildLoadResultMethod!.Invoke(
registration,
new object?[]
{
Path.Combine(_rootPath, "monster"),
null,
new List<MonsterConfigStub>
{
new()
{
Id = 1,
Name = "Slime",
Hp = 10
}
},
new List<YamlConfigReferenceUsage>(),
cancellationTokenSource.Token
}));
// 反射调用同步私有方法时会把原始异常包装为 TargetInvocationException。
Assert.That(exception!.InnerException, Is.InstanceOf<OperationCanceledException>());
}
/// <summary>
/// 验证依赖关系仅来自 <c>contains</c> 子 schema 时,热重载仍会追踪该依赖并在目标表破坏引用后回滚。
/// </summary>
@ -2928,7 +3055,7 @@ public class YamlConfigLoaderTests
Assert.That(exception!.ParamName, Is.EqualTo("options"));
}
/// <summary>
/// 验证热重载失败时会保留旧表状态,并通过失败回调暴露诊断信息。
/// </summary>
@ -3372,6 +3499,22 @@ public class YamlConfigLoaderTests
CreateConfigFile(relativePath, content);
}
private static object GetSingleYamlTableRegistration(YamlConfigLoader loader)
{
var registrationsField = typeof(YamlConfigLoader).GetField(
"_registrations",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(registrationsField, Is.Not.Null);
var registrations = registrationsField!.GetValue(loader) as System.Collections.IList;
Assert.That(registrations, Is.Not.Null);
Assert.That(registrations!.Count, Is.EqualTo(1));
return registrations[0]!;
}
/// <summary>
/// 在限定时间内等待异步任务完成,避免文件监听测试无限挂起。
/// </summary>

View File

@ -137,12 +137,10 @@ public sealed class SettingsModelTests
var migrationMapLock = lockField!.GetValue(model);
Assert.That(migrationMapLock, Is.Not.Null);
Task initializeTask;
Task registerTask;
lock (migrationMapLock!)
var tasks = WithSynchronizationLockHeld(migrationMapLock!, () =>
{
initializeTask = Task.Run(() => model.InitializeAsync());
registerTask = Task.Run(() => model.RegisterMigration(new TestLatestSettingsMigrationV2ToV3()));
var initializeTask = Task.Run(() => model.InitializeAsync());
var registerTask = Task.Run(() => model.RegisterMigration(new TestLatestSettingsMigrationV2ToV3()));
Thread.Sleep(50);
@ -151,7 +149,11 @@ public sealed class SettingsModelTests
Assert.That(initializeTask.IsCompleted, Is.False);
Assert.That(registerTask.IsCompleted, Is.False);
});
}
return (initializeTask, registerTask);
});
var (initializeTask, registerTask) = tasks;
await Task.WhenAll(initializeTask, registerTask);
@ -171,6 +173,35 @@ public sealed class SettingsModelTests
});
}
/// <summary>
/// 以与被测代码相同的同步原语持有反射获取到的锁对象,避免在 .NET 9+ 上把 <see cref="System.Threading.Lock" />
/// 退化成 <see cref="Monitor" /> 语义,导致并发测试误判。
/// </summary>
/// <param name="syncRoot">通过反射读取到的私有锁字段。</param>
/// <typeparam name="TResult">持锁代码返回的结果类型。</typeparam>
/// <param name="action">持锁期间执行的断言与并发调度逻辑。</param>
/// <returns>持锁代码的返回值。</returns>
private static TResult WithSynchronizationLockHeld<TResult>(object syncRoot, Func<TResult> action)
{
ArgumentNullException.ThrowIfNull(syncRoot);
ArgumentNullException.ThrowIfNull(action);
#if NET9_0_OR_GREATER
if (syncRoot is System.Threading.Lock typedLock)
{
using (typedLock.EnterScope())
{
return action();
}
}
#endif
lock (syncRoot)
{
return action();
}
}
private sealed class TestSettingsData : ISettingsData
{
public string Value { get; set; } = "default";

View File

@ -20,7 +20,11 @@ public sealed class GameConfigBootstrap : IDisposable
// All lifecycle transitions share one gate so initialization, hot-reload startup,
// stop, and disposal never publish half-finished state to concurrent callers.
#if NET9_0_OR_GREATER
private readonly Lock _stateGate = new();
#else
private readonly object _stateGate = new();
#endif
private readonly GameConfigBootstrapOptions _options;
private IUnRegister? _hotReload;
private YamlConfigLoader? _loader;
@ -210,67 +214,16 @@ public sealed class GameConfigBootstrap : IDisposable
/// </exception>
public void StartHotReload(YamlConfigHotReloadOptions? options = null)
{
YamlConfigLoader loader;
lock (_stateGate)
{
ThrowIfDisposedCore();
loader = _loader ?? throw new InvalidOperationException(
"Hot reload can only be started after the initial config load succeeds.");
if (_isStartingHotReload || _hotReload != null)
{
throw new InvalidOperationException("Hot reload is already enabled.");
}
_isStartingHotReload = true;
_stopHotReloadAfterStart = false;
}
var loader = BeginHotReloadStart();
IUnRegister? hotReload = null;
try
{
hotReload = loader.EnableHotReload(Registry, options);
var shouldStop = false;
lock (_stateGate)
{
try
{
ThrowIfDisposedCore();
// Stop/Dispose may arrive while the watcher is being created. In that
// case, release the new handle immediately instead of publishing it.
if (_stopHotReloadAfterStart)
{
shouldStop = true;
_stopHotReloadAfterStart = false;
}
else
{
_hotReload = hotReload;
hotReload = null;
}
}
finally
{
_isStartingHotReload = false;
}
}
if (shouldStop)
{
hotReload?.UnRegister();
}
hotReload = CompleteHotReloadStart(hotReload);
}
catch
{
lock (_stateGate)
{
_isStartingHotReload = false;
_stopHotReloadAfterStart = false;
}
ResetHotReloadStartAfterFailure();
hotReload?.UnRegister();
throw;
}
@ -332,4 +285,70 @@ public sealed class GameConfigBootstrap : IDisposable
throw new ObjectDisposedException(nameof(GameConfigBootstrap));
}
}
private YamlConfigLoader BeginHotReloadStart()
{
lock (_stateGate)
{
ThrowIfDisposedCore();
var loader = _loader ?? throw new InvalidOperationException(
"Hot reload can only be started after the initial config load succeeds.");
if (_isStartingHotReload || _hotReload != null)
{
throw new InvalidOperationException("Hot reload is already enabled.");
}
_isStartingHotReload = true;
_stopHotReloadAfterStart = false;
return loader;
}
}
private IUnRegister? CompleteHotReloadStart(IUnRegister? hotReload)
{
var shouldStop = false;
lock (_stateGate)
{
try
{
ThrowIfDisposedCore();
// Stop/Dispose may arrive while the watcher is being created. In that
// case, release the new handle immediately instead of publishing it.
if (_stopHotReloadAfterStart)
{
shouldStop = true;
_stopHotReloadAfterStart = false;
}
else
{
_hotReload = hotReload;
hotReload = null;
}
}
finally
{
_isStartingHotReload = false;
}
}
if (shouldStop)
{
hotReload?.UnRegister();
return null;
}
return hotReload;
}
private void ResetHotReloadStartAfterFailure()
{
lock (_stateGate)
{
_isStartingHotReload = false;
_stopHotReloadAfterStart = false;
}
}
}

View File

@ -1,4 +1,6 @@
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using GFramework.Core.Abstractions.Events;
using GFramework.Game.Abstractions.Config;
using YamlDotNet.Serialization;
@ -472,93 +474,178 @@ public sealed class YamlConfigLoader : IConfigLoader
IDeserializer deserializer,
CancellationToken cancellationToken)
{
var directoryPath = Path.Combine(rootPath, RelativePath);
if (!Directory.Exists(directoryPath))
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConfigDirectoryNotFound,
Name,
$"Config directory '{directoryPath}' was not found for table '{Name}'.",
configDirectoryPath: directoryPath);
}
YamlConfigSchema? schema = null;
IReadOnlyCollection<string> referencedTableNames = Array.Empty<string>();
if (!string.IsNullOrEmpty(SchemaRelativePath))
{
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
schema = await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken)
.ConfigureAwait(false);
referencedTableNames = schema.ReferencedTableNames;
}
var directoryPath = GetValidatedDirectoryPath(rootPath);
var schema = await LoadSchemaAsync(rootPath, cancellationToken).ConfigureAwait(false);
var referenceUsages = new List<YamlConfigReferenceUsage>();
var values = await LoadValuesAsync(
directoryPath,
deserializer,
schema,
referenceUsages,
cancellationToken)
.ConfigureAwait(false);
return BuildLoadResult(directoryPath, schema, values, referenceUsages, cancellationToken);
}
private string GetValidatedDirectoryPath(string rootPath)
{
var directoryPath = Path.Combine(rootPath, RelativePath);
if (Directory.Exists(directoryPath))
{
return directoryPath;
}
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConfigDirectoryNotFound,
Name,
$"Config directory '{directoryPath}' was not found for table '{Name}'.",
configDirectoryPath: directoryPath);
}
private async Task<YamlConfigSchema?> LoadSchemaAsync(string rootPath, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(SchemaRelativePath))
{
return null;
}
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
return await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken).ConfigureAwait(false);
}
private async Task<List<TValue>> LoadValuesAsync(
string directoryPath,
IDeserializer deserializer,
YamlConfigSchema? schema,
List<YamlConfigReferenceUsage> referenceUsages,
CancellationToken cancellationToken)
{
var values = new List<TValue>();
var files = Directory
foreach (var file in GetYamlFiles(directoryPath))
{
cancellationToken.ThrowIfCancellationRequested();
var yaml = await ReadYamlAsync(directoryPath, file, schema, cancellationToken).ConfigureAwait(false);
CollectReferenceUsages(referenceUsages, schema, file, yaml);
values.Add(DeserializeValue(deserializer, directoryPath, file, schema, yaml, cancellationToken));
}
return values;
}
private static string[] GetYamlFiles(string directoryPath)
{
return Directory
.EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly)
.Where(static path =>
path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase))
.OrderBy(static path => path, StringComparer.Ordinal)
.ToArray();
}
foreach (var file in files)
{
cancellationToken.ThrowIfCancellationRequested();
string yaml;
try
{
yaml = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConfigFileReadFailed,
Name,
$"Failed to read config file '{file}' for table '{Name}'.",
configDirectoryPath: directoryPath,
yamlPath: file,
schemaPath: schema?.SchemaPath,
innerException: exception);
}
if (schema != null)
{
// 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。
referenceUsages.AddRange(
YamlConfigSchemaValidator.ValidateAndCollectReferences(Name, schema, file, yaml));
}
try
{
var value = deserializer.Deserialize<TValue>(yaml);
if (value == null)
{
throw new InvalidOperationException("YAML content was deserialized to null.");
}
values.Add(value);
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.DeserializationFailed,
Name,
$"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.",
configDirectoryPath: directoryPath,
yamlPath: file,
schemaPath: schema?.SchemaPath,
detail: $"Target CLR type: {typeof(TValue).FullName}.",
innerException: exception);
}
}
private async Task<string> ReadYamlAsync(
string directoryPath,
string file,
YamlConfigSchema? schema,
CancellationToken cancellationToken)
{
try
{
return await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// 保留原始取消语义,避免热重载把会话级取消误报为配置读取失败。
throw;
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.ConfigFileReadFailed,
Name,
$"Failed to read config file '{file}' for table '{Name}'.",
configDirectoryPath: directoryPath,
yamlPath: file,
schemaPath: schema?.SchemaPath,
innerException: exception);
}
}
private void CollectReferenceUsages(
List<YamlConfigReferenceUsage> referenceUsages,
YamlConfigSchema? schema,
string file,
string yaml)
{
if (schema == null)
{
return;
}
// 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。
referenceUsages.AddRange(
YamlConfigSchemaValidator.ValidateAndCollectReferences(Name, schema, file, yaml));
}
private TValue DeserializeValue(
IDeserializer deserializer,
string directoryPath,
string file,
YamlConfigSchema? schema,
string yaml,
CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var value = deserializer.Deserialize<TValue>(yaml);
if (value != null)
{
return value;
}
throw new InvalidOperationException("YAML content was deserialized to null.");
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// 同步反序列化阶段也要透传会话级取消,避免把停止加载误报为 YAML 解析失败。
throw;
}
catch (Exception exception)
{
throw ConfigLoadExceptionFactory.Create(
ConfigLoadFailureKind.DeserializationFailed,
Name,
$"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.",
configDirectoryPath: directoryPath,
yamlPath: file,
schemaPath: schema?.SchemaPath,
detail: $"Target CLR type: {typeof(TValue).FullName}.",
innerException: exception);
}
}
private YamlTableLoadResult BuildLoadResult(
string directoryPath,
YamlConfigSchema? schema,
List<TValue> values,
List<YamlConfigReferenceUsage> referenceUsages,
CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var table = new InMemoryConfigTable<TKey, TValue>(values, _keySelector, _comparer);
return new YamlTableLoadResult(Name, table, referencedTableNames, referenceUsages);
return new YamlTableLoadResult(
Name,
table,
schema?.ReferencedTableNames ?? Array.Empty<string>(),
referenceUsages);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// 构建最终配置表时继续保留原始取消语义,避免热重载把提交前取消记录成构表失败。
throw;
}
catch (Exception exception)
{
@ -630,6 +717,12 @@ public sealed class YamlConfigLoader : IConfigLoader
/// </summary>
private static class CrossTableReferenceValidator
{
private delegate bool IntegerTryParseDelegate<T>(
string? value,
NumberStyles style,
IFormatProvider? provider,
out T result);
/// <summary>
/// 使用本轮新加载结果与注册表中保留的旧表,一起验证跨表引用是否全部有效。
/// </summary>
@ -754,59 +847,15 @@ public sealed class YamlConfigLoader : IConfigLoader
convertedKey = null;
errorMessage = string.Empty;
if (targetKeyType == typeof(int) &&
int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
if (TryConvertIntegerKey<int>(rawValue, targetKeyType, typeof(int), int.TryParse, out convertedKey) ||
TryConvertIntegerKey<long>(rawValue, targetKeyType, typeof(long), long.TryParse, out convertedKey) ||
TryConvertIntegerKey<short>(rawValue, targetKeyType, typeof(short), short.TryParse, out convertedKey) ||
TryConvertIntegerKey<byte>(rawValue, targetKeyType, typeof(byte), byte.TryParse, out convertedKey) ||
TryConvertIntegerKey<uint>(rawValue, targetKeyType, typeof(uint), uint.TryParse, out convertedKey) ||
TryConvertIntegerKey<ulong>(rawValue, targetKeyType, typeof(ulong), ulong.TryParse, out convertedKey) ||
TryConvertIntegerKey<ushort>(rawValue, targetKeyType, typeof(ushort), ushort.TryParse, out convertedKey) ||
TryConvertIntegerKey<sbyte>(rawValue, targetKeyType, typeof(sbyte), sbyte.TryParse, out convertedKey))
{
convertedKey = intValue;
return true;
}
if (targetKeyType == typeof(long) &&
long.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue))
{
convertedKey = longValue;
return true;
}
if (targetKeyType == typeof(short) &&
short.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var shortValue))
{
convertedKey = shortValue;
return true;
}
if (targetKeyType == typeof(byte) &&
byte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var byteValue))
{
convertedKey = byteValue;
return true;
}
if (targetKeyType == typeof(uint) &&
uint.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uintValue))
{
convertedKey = uintValue;
return true;
}
if (targetKeyType == typeof(ulong) &&
ulong.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ulongValue))
{
convertedKey = ulongValue;
return true;
}
if (targetKeyType == typeof(ushort) &&
ushort.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ushortValue))
{
convertedKey = ushortValue;
return true;
}
if (targetKeyType == typeof(sbyte) &&
sbyte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sbyteValue))
{
convertedKey = sbyteValue;
return true;
}
@ -815,6 +864,25 @@ public sealed class YamlConfigLoader : IConfigLoader
return false;
}
private static bool TryConvertIntegerKey<T>(
string rawValue,
Type targetKeyType,
Type supportedType,
IntegerTryParseDelegate<T> tryParse,
out object? convertedKey)
where T : struct
{
convertedKey = null;
if (targetKeyType != supportedType ||
!tryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue))
{
return false;
}
convertedKey = parsedValue;
return true;
}
private static bool ContainsKey(IConfigTable table, object key)
{
var tableInterface = table.GetType()
@ -838,7 +906,13 @@ public sealed class YamlConfigLoader : IConfigLoader
new(StringComparer.Ordinal);
private readonly IDeserializer _deserializer;
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly Lock _gate = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _gate = new();
#endif
private readonly Action<string>? _onTableReloaded;
private readonly Action<string, Exception>? _onTableReloadFailed;
private readonly Dictionary<string, IYamlTableRegistration> _registrations = new(StringComparer.Ordinal);
@ -1121,7 +1195,7 @@ public sealed class YamlConfigLoader : IConfigLoader
foreach (var dependency in _dependenciesByTable)
{
if (!dependency.Value.Contains(currentTableName))
if (!ContainsDependency(dependency.Value, currentTableName))
{
continue;
}
@ -1138,6 +1212,14 @@ public sealed class YamlConfigLoader : IConfigLoader
.ToArray();
}
private static bool ContainsDependency(
IReadOnlyCollection<string> dependencies,
string tableName)
{
return dependencies.Any(
dependency => string.Equals(dependency, tableName, StringComparison.Ordinal));
}
private void InvokeReloaded(string tableName)
{
if (_onTableReloaded == null)

View File

@ -52,7 +52,9 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
var key = location.ToStorageKey();
// 检查存储中是否存在指定键的数据
T result = await Storage.ExistsAsync(key) ? await Storage.ReadAsync<T>(key) : new T();
T result = await Storage.ExistsAsync(key).ConfigureAwait(false)
? await Storage.ReadAsync<T>(key).ConfigureAwait(false)
: new T();
// 如果启用事件功能,则发送数据加载完成事件
if (_options.EnableEvents)
@ -70,7 +72,7 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
public async Task SaveAsync<T>(IDataLocation location, T data)
where T : class, IData
{
await SaveCoreAsync(location, data, emitSavedEvent: true);
await SaveCoreAsync(location, data, emitSavedEvent: true).ConfigureAwait(false);
}
/// <summary>
@ -91,12 +93,12 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
{
var key = location.ToStorageKey();
if (!await Storage.ExistsAsync(key))
if (!await Storage.ExistsAsync(key).ConfigureAwait(false))
{
return;
}
await Storage.DeleteAsync(key);
await Storage.DeleteAsync(key).ConfigureAwait(false);
if (_options.EnableEvents)
this.SendEvent(new DataDeletedEvent(location));
}
@ -113,7 +115,7 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
// 但抑制逐项 DataSavedEvent避免监听器对同一批次收到重复语义的事件。
foreach (var (location, data) in valueTuples)
{
await SaveCoreUntypedAsync(location, data, emitSavedEvent: false);
await SaveCoreUntypedAsync(location, data, emitSavedEvent: false).ConfigureAwait(false);
}
if (_options.EnableEvents)
@ -140,8 +142,8 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
{
var key = location.ToStorageKey();
await BackupIfNeededAsync<T>(key);
await Storage.WriteAsync(key, data);
await BackupIfNeededAsync<T>(key).ConfigureAwait(false);
await Storage.WriteAsync(key, data).ConfigureAwait(false);
if (emitSavedEvent && _options.EnableEvents)
{
@ -156,14 +158,14 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
private async Task BackupIfNeededAsync<T>(string key)
where T : class, IData
{
if (!_options.AutoBackup || !await Storage.ExistsAsync(key))
if (!_options.AutoBackup || !await Storage.ExistsAsync(key).ConfigureAwait(false))
{
return;
}
var backupKey = $"{key}.backup";
var existing = await Storage.ReadAsync<T>(key);
await Storage.WriteAsync(backupKey, existing);
var existing = await Storage.ReadAsync<T>(key).ConfigureAwait(false);
await Storage.WriteAsync(backupKey, existing).ConfigureAwait(false);
}
/// <summary>

View File

@ -33,7 +33,13 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
{
private readonly SaveConfiguration _config;
private readonly Dictionary<int, ISaveMigration<TSaveData>> _migrations = new();
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _migrationsLock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _migrationsLock = new();
#endif
private readonly IStorage _rootStorage;
/// <summary>
@ -99,7 +105,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
public async Task<bool> ExistsAsync(int slot)
{
var storage = GetSlotStorage(slot);
return await storage.ExistsAsync(_config.SaveFileName);
return await storage.ExistsAsync(_config.SaveFileName).ConfigureAwait(false);
}
/// <summary>
@ -111,10 +117,10 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
{
var storage = GetSlotStorage(slot);
if (await storage.ExistsAsync(_config.SaveFileName))
if (await storage.ExistsAsync(_config.SaveFileName).ConfigureAwait(false))
{
var loaded = await storage.ReadAsync<TSaveData>(_config.SaveFileName);
return await MigrateIfNeededAsync(slot, storage, loaded);
var loaded = await storage.ReadAsync<TSaveData>(_config.SaveFileName).ConfigureAwait(false);
return await MigrateIfNeededAsync(slot, storage, loaded).ConfigureAwait(false);
}
return new TSaveData();
@ -130,11 +136,11 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
var slotPath = $"{_config.SaveSlotPrefix}{slot}";
// 确保槽位目录存在
if (!await _rootStorage.DirectoryExistsAsync(slotPath))
await _rootStorage.CreateDirectoryAsync(slotPath);
if (!await _rootStorage.DirectoryExistsAsync(slotPath).ConfigureAwait(false))
await _rootStorage.CreateDirectoryAsync(slotPath).ConfigureAwait(false);
var storage = GetSlotStorage(slot);
await storage.WriteAsync(_config.SaveFileName, data);
await storage.WriteAsync(_config.SaveFileName, data).ConfigureAwait(false);
}
/// <summary>
@ -144,7 +150,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
public async Task DeleteAsync(int slot)
{
var storage = GetSlotStorage(slot);
await storage.DeleteAsync(_config.SaveFileName);
await storage.DeleteAsync(_config.SaveFileName).ConfigureAwait(false);
}
/// <summary>
@ -154,7 +160,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
public async Task<IReadOnlyList<int>> ListSlotsAsync()
{
// 列举所有槽位目录
var directories = await _rootStorage.ListDirectoriesAsync();
var directories = await _rootStorage.ListDirectoriesAsync().ConfigureAwait(false);
var slots = new List<int>();
@ -171,7 +177,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
// 直接检查存档文件是否存在,避免重复创建 ScopedStorage
var saveFilePath = $"{dirName}/{_config.SaveFileName}";
if (await _rootStorage.ExistsAsync(saveFilePath))
if (await _rootStorage.ExistsAsync(saveFilePath).ConfigureAwait(false))
slots.Add(slot);
}
@ -246,7 +252,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
$"{typeof(TSaveData).Name} in slot {slot}",
"save migration");
await storage.WriteAsync(_config.SaveFileName, migrated);
await storage.WriteAsync(_config.SaveFileName, migrated).ConfigureAwait(false);
return migrated;
}

View File

@ -37,7 +37,7 @@ public class UnifiedSettingsDataRepository(
{
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly DataRepositoryOptions _options = options ?? new DataRepositoryOptions();
private readonly Dictionary<string, Type> _typeRegistry = new();
private readonly Dictionary<string, Type> _typeRegistry = new(StringComparer.Ordinal);
private UnifiedSettingsFile? _file;
private bool _loaded;
private IRuntimeTypeSerializer? _serializer = serializer;
@ -67,7 +67,7 @@ public class UnifiedSettingsDataRepository(
public async Task<T> LoadAsync<T>(IDataLocation location)
where T : class, IData, new()
{
await EnsureLoadedAsync();
await EnsureLoadedAsync().ConfigureAwait(false);
var key = location.Key;
var result = _file!.Sections.TryGetValue(key, out var raw) ? Serializer.Deserialize<T>(raw) : new T();
if (_options.EnableEvents)
@ -85,8 +85,9 @@ public class UnifiedSettingsDataRepository(
public async Task SaveAsync<T>(IDataLocation location, T data)
where T : class, IData
{
await EnsureLoadedAsync();
await MutateAndPersistAsync(file => file.Sections[location.Key] = Serializer.Serialize(data));
await EnsureLoadedAsync().ConfigureAwait(false);
await MutateAndPersistAsync(file => file.Sections[location.Key] = Serializer.Serialize(data))
.ConfigureAwait(false);
if (_options.EnableEvents)
{
@ -101,7 +102,7 @@ public class UnifiedSettingsDataRepository(
/// <returns>如果数据存在则返回true否则返回false</returns>
public async Task<bool> ExistsAsync(IDataLocation location)
{
await EnsureLoadedAsync();
await EnsureLoadedAsync().ConfigureAwait(false);
return File.Sections.ContainsKey(location.Key);
}
@ -112,10 +113,10 @@ public class UnifiedSettingsDataRepository(
/// <returns>异步操作任务</returns>
public async Task DeleteAsync(IDataLocation location)
{
await EnsureLoadedAsync();
await EnsureLoadedAsync().ConfigureAwait(false);
var removed = false;
await _lock.WaitAsync();
await _lock.WaitAsync().ConfigureAwait(false);
try
{
var currentFile = File;
@ -126,7 +127,7 @@ public class UnifiedSettingsDataRepository(
return;
}
await WriteUnifiedFileCoreAsync(currentFile, nextFile);
await WriteUnifiedFileCoreAsync(currentFile, nextFile).ConfigureAwait(false);
_file = nextFile;
}
finally
@ -148,17 +149,18 @@ public class UnifiedSettingsDataRepository(
public async Task SaveAllAsync(
IEnumerable<(IDataLocation location, IData data)> dataList)
{
await EnsureLoadedAsync();
await EnsureLoadedAsync().ConfigureAwait(false);
var valueTuples = dataList.ToList();
await MutateAndPersistAsync(file =>
{
foreach (var (location, data) in valueTuples)
{
file.Sections[location.Key] = Serializer.Serialize(data);
}
});
foreach (var (location, data) in valueTuples)
{
file.Sections[location.Key] = Serializer.Serialize(data);
}
})
.ConfigureAwait(false);
if (_options.EnableEvents)
this.SendEvent(new DataBatchSavedEvent(valueTuples));
@ -170,9 +172,9 @@ public class UnifiedSettingsDataRepository(
/// <returns>包含所有数据项的字典,键为数据位置键,值为数据对象</returns>
public async Task<IDictionary<string, IData>> LoadAllAsync()
{
await EnsureLoadedAsync();
await EnsureLoadedAsync().ConfigureAwait(false);
var result = new Dictionary<string, IData>();
var result = new Dictionary<string, IData>(StringComparer.Ordinal);
foreach (var (key, raw) in File.Sections)
{
@ -216,15 +218,15 @@ public class UnifiedSettingsDataRepository(
{
if (_loaded) return;
await _lock.WaitAsync();
await _lock.WaitAsync().ConfigureAwait(false);
try
{
if (_loaded) return;
var key = UnifiedKey;
_file = await Storage.ExistsAsync(key)
? await Storage.ReadAsync<UnifiedSettingsFile>(key)
_file = await Storage.ExistsAsync(key).ConfigureAwait(false)
? await Storage.ReadAsync<UnifiedSettingsFile>(key).ConfigureAwait(false)
: new UnifiedSettingsFile { Version = 1 };
_loaded = true;
@ -241,7 +243,7 @@ public class UnifiedSettingsDataRepository(
/// </summary>
private async Task MutateAndPersistAsync(Action<UnifiedSettingsFile> mutation)
{
await _lock.WaitAsync();
await _lock.WaitAsync().ConfigureAwait(false);
try
{
var currentFile = File;
@ -250,7 +252,7 @@ public class UnifiedSettingsDataRepository(
// 先在副本上计算“下一份已提交状态”,只有底层持久化成功后才交换缓存,
// 这样即使备份或写入失败,也不会把未提交修改留在内存快照里。
mutation(nextFile);
await WriteUnifiedFileCoreAsync(currentFile, nextFile);
await WriteUnifiedFileCoreAsync(currentFile, nextFile).ConfigureAwait(false);
_file = nextFile;
}
finally
@ -270,13 +272,13 @@ public class UnifiedSettingsDataRepository(
/// <param name="nextFile">即将提交的新统一文件快照。</param>
private async Task WriteUnifiedFileCoreAsync(UnifiedSettingsFile currentFile, UnifiedSettingsFile nextFile)
{
if (_options.AutoBackup && await Storage.ExistsAsync(UnifiedKey))
if (_options.AutoBackup && await Storage.ExistsAsync(UnifiedKey).ConfigureAwait(false))
{
var backupKey = $"{UnifiedKey}.backup";
await Storage.WriteAsync(backupKey, currentFile);
await Storage.WriteAsync(backupKey, currentFile).ConfigureAwait(false);
}
await Storage.WriteAsync(UnifiedKey, nextFile);
await Storage.WriteAsync(UnifiedKey, nextFile).ConfigureAwait(false);
}
/// <summary>

View File

@ -31,8 +31,7 @@
- 引擎适配包或项目内适配层
- 本包提供的是“引擎无关”的核心逻辑和基类。
- 真正和 Godot、Unity、MonoGame 等引擎对象打交道的工厂、根节点、资源注册表,通常在相邻引擎包或游戏项目内实现。
- 仓库内 `ai-libs/` 下的只读参考实现通常也是这样接入:配置文件 IO 由 `GFramework.Godot.Config` 适配,
UI / Scene factory 与 root 由项目自己提供。
- 典型项目里,配置文件 IO 会交给宿主适配层处理UI / Scene 的 factory 与 root 则继续由项目自己提供。
## 子系统地图
@ -73,7 +72,7 @@
- `SaveConfiguration`
- 槽位目录、文件名、前缀等约定
`ai-libs/` 下已验证参考实现的常见接法:
常见接法:
- 设置持久化使用 `UnifiedSettingsDataRepository`
- 存档使用 `SaveRepository<GameSaveData>`
@ -96,7 +95,7 @@
- `Setting/Events/*`
- 设置初始化、应用、保存、重置相关事件
`ai-libs/` 下已验证参考实现的常见接法:
常见接法:
- 在模型模块中创建 `SettingsModel<ISettingsDataRepository>`
- 注册多个 applicator
@ -122,7 +121,6 @@
对应文档:
- [存储系统](../docs/zh-CN/game/storage.md)
- [Storage 子模块说明](./Storage/ReadMe.md)
### `Serializer/`
@ -149,7 +147,7 @@
- `Scene/Handler/*``UI/Handler/*`
- 默认转换处理器基类与日志处理器
`ai-libs/` 下已验证参考实现的常见接法:
常见接法:
- 项目自定义 `SceneRouter : SceneRouterBase`
- 项目自定义 `UiRouter : UiRouterBase`
@ -266,7 +264,7 @@ await settingsSystem.ApplyAll();
await settingsSystem.SaveAll();
```
`ai-libs/` 下的只读参考实现目前也是按这个思路接入,只是底层存储换成了 Godot 适配实现。
在 Godot 项目中也可以沿用同一思路,只是底层存储通常换成宿主侧适配实现。
### 3. 接入静态 YAML 配置
@ -324,9 +322,9 @@ public sealed class MyUiRouter : UiRouterBase
这类 router 适合作为你的项目层或引擎适配层代码,而不是直接修改本包。
## `ai-libs/` 里的参考接入线索
## 典型项目分层方式
当前仓库内的只读参考实现,对本包的使用大致分成三层:
典型项目对本包的使用大致分成三层:
- 配置
- 项目级配置宿主类型使用生成表元数据与 YAML loader 完成配置注册

View File

@ -218,7 +218,7 @@ public abstract class RouterBase<TRoute, TContext> : AbstractSystem
/// <returns>如果栈中包含指定路由返回 true,否则返回 false</returns>
public bool Contains(string routeKey)
{
return Stack.Any(r => r.Key == routeKey);
return Stack.Any(r => string.Equals(r.Key, routeKey, StringComparison.Ordinal));
}
/// <summary>
@ -237,7 +237,7 @@ public abstract class RouterBase<TRoute, TContext> : AbstractSystem
/// <returns>如果栈顶是指定路由返回 true,否则返回 false</returns>
public bool IsTop(string routeKey)
{
return Stack.Count != 0 && Stack.Peek().Key.Equals(routeKey);
return Stack.Count != 0 && string.Equals(Stack.Peek().Key, routeKey, StringComparison.Ordinal);
}
#endregion

View File

@ -11,6 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Extensions;
using GFramework.Core.Logging;
@ -82,7 +83,7 @@ public abstract class SceneRouterBase
string sceneKey,
ISceneEnterParam? param = null)
{
await _transitionLock.WaitAsync();
await _transitionLock.WaitAsync().ConfigureAwait(true);
try
{
IsTransitioning = true;
@ -111,7 +112,7 @@ public abstract class SceneRouterBase
/// <returns>如果场景在栈中返回true否则返回false。</returns>
public new bool Contains(string sceneKey)
{
return Stack.Any(s => s.Key == sceneKey);
return Stack.Any(s => string.Equals(s.Key, sceneKey, StringComparison.Ordinal));
}
#endregion
@ -184,7 +185,7 @@ public abstract class SceneRouterBase
string sceneKey,
ISceneEnterParam? param = null)
{
await _transitionLock.WaitAsync();
await _transitionLock.WaitAsync().ConfigureAwait(true);
try
{
IsTransitioning = true;
@ -220,7 +221,7 @@ public abstract class SceneRouterBase
}
// 守卫检查
if (!await ExecuteEnterGuardsAsync(sceneKey, param))
if (!await ExecuteEnterGuardsAsync(sceneKey, param).ConfigureAwait(true))
{
Log.Warn("Push blocked by guard: {0}", sceneKey);
return;
@ -233,20 +234,20 @@ public abstract class SceneRouterBase
Root!.AddScene(scene);
// 加载资源
await scene.OnLoadAsync(param);
await scene.OnLoadAsync(param).ConfigureAwait(true);
// 暂停当前场景
if (Stack.Count > 0)
{
var current = Stack.Peek();
await current.OnPauseAsync();
await current.OnPauseAsync().ConfigureAwait(true);
}
// 压入栈
Stack.Push(scene);
// 进入场景
await scene.OnEnterAsync();
await scene.OnEnterAsync().ConfigureAwait(true);
Log.Debug("Push Scene: {0}, stackCount={1}",
sceneKey, Stack.Count);
@ -262,7 +263,7 @@ public abstract class SceneRouterBase
/// <returns>异步任务。</returns>
public async ValueTask PopAsync()
{
await _transitionLock.WaitAsync();
await _transitionLock.WaitAsync().ConfigureAwait(true);
try
{
IsTransitioning = true;
@ -293,7 +294,7 @@ public abstract class SceneRouterBase
var top = Stack.Peek();
// 守卫检查
if (!await ExecuteLeaveGuardsAsync(top.Key))
if (!await ExecuteLeaveGuardsAsync(top.Key).ConfigureAwait(true))
{
Log.Warn("Pop blocked by guard: {0}", top.Key);
return;
@ -302,10 +303,10 @@ public abstract class SceneRouterBase
Stack.Pop();
// 退出场景
await top.OnExitAsync();
await top.OnExitAsync().ConfigureAwait(true);
// 卸载资源
await top.OnUnloadAsync();
await top.OnUnloadAsync().ConfigureAwait(true);
// 从场景树移除
Root!.RemoveScene(top);
@ -314,7 +315,7 @@ public abstract class SceneRouterBase
if (Stack.Count > 0)
{
var next = Stack.Peek();
await next.OnResumeAsync();
await next.OnResumeAsync().ConfigureAwait(true);
}
Log.Debug("Pop Scene, stackCount={0}", Stack.Count);
@ -330,7 +331,7 @@ public abstract class SceneRouterBase
/// <returns>异步任务。</returns>
public async ValueTask ClearAsync()
{
await _transitionLock.WaitAsync();
await _transitionLock.WaitAsync().ConfigureAwait(true);
try
{
IsTransitioning = true;

View File

@ -29,7 +29,13 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
private readonly ConcurrentDictionary<Type, ISettingsData> _data = new();
private readonly ConcurrentDictionary<Type, Dictionary<int, ISettingsMigration>> _migrationCache = new();
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _migrationMapLock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _migrationMapLock = new();
#endif
private readonly ConcurrentDictionary<(Type type, int from), ISettingsMigration> _migrations = new();
private volatile bool _initialized;
@ -169,7 +175,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
try
{
allData = await DataRepository.LoadAllAsync();
allData = await DataRepository.LoadAllAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
@ -213,7 +219,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
try
{
var location = LocationProvider.GetLocation(data.GetType());
await DataRepository.SaveAsync(location, data);
await DataRepository.SaveAsync(location, data).ConfigureAwait(false);
}
catch (Exception ex)
{
@ -231,7 +237,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
foreach (var applicator in _applicators)
try
{
await applicator.Value.ApplyAsync();
await applicator.Value.ApplyAsync().ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@ -51,8 +51,8 @@ public static class UiInteractionProfiles
{
return action switch
{
UiInputAction.Cancel => (profile.CapturedActions & UiInputActionMask.Cancel) != 0,
UiInputAction.Confirm => (profile.CapturedActions & UiInputActionMask.Confirm) != 0,
UiInputAction.Cancel => (profile.CapturedActions & UiInputActionMask.Cancel) != UiInputActionMask.None,
UiInputAction.Confirm => (profile.CapturedActions & UiInputActionMask.Confirm) != UiInputActionMask.None,
_ => false
};
}

View File

@ -260,7 +260,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
/// <returns>如果栈顶是指定UI则返回true否则返回false</returns>
public new bool IsTop(string uiKey)
{
return Stack.Count != 0 && Stack.Peek().Key.Equals(uiKey);
return Stack.Count != 0 && string.Equals(Stack.Peek().Key, uiKey, StringComparison.Ordinal);
}
/// <summary>
@ -270,7 +270,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
/// <returns>如果栈中包含指定UI则返回true否则返回false</returns>
public new bool Contains(string uiKey)
{
return Stack.Any(p => p.Key.Equals(uiKey));
return Stack.Any(p => string.Equals(p.Key, uiKey, StringComparison.Ordinal));
}
/// <summary>
@ -293,7 +293,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
public UiHandle Show(string uiKey, UiLayer layer, IUiPageEnterParam? param = null)
{
if (layer == UiLayer.Page)
throw new ArgumentException("Use Push() for Page layer");
throw new ArgumentException("Use Push() for Page layer", nameof(layer));
// 创建实例
var page = _factory.Create(uiKey);
@ -311,7 +311,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
public UiHandle Show(IUiPageBehavior page, UiLayer layer)
{
if (layer == UiLayer.Page)
throw new ArgumentException("Use Push() for Page layer");
throw new ArgumentException("Use Push() for Page layer", nameof(layer));
return ShowInternal(page, layer, null);
}
@ -414,7 +414,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
return Array.Empty<UiHandle>();
return layerDict
.Where(kvp => kvp.Value.Key.Equals(uiKey))
.Where(kvp => string.Equals(kvp.Value.Key, uiKey, StringComparison.Ordinal))
.Select(kvp => new UiHandle(uiKey, kvp.Key, layer))
.ToList();
}
@ -593,14 +593,18 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
var handle = new UiHandle(page.Key, instanceId, layer);
// 初始化层级字典
if (!_layers.ContainsKey(layer))
_layers[layer] = new Dictionary<string, IUiPageBehavior>();
if (!_layers.TryGetValue(layer, out var layerDict))
{
layerDict = new Dictionary<string, IUiPageBehavior>(StringComparer.Ordinal);
_layers[layer] = layerDict;
}
// 设置句柄
page.Handle = handle;
var layerDict = _layers[layer];
// 检查重入性
if (!page.IsReentrant && layerDict.Values.Any(p => p.Key == page.Key))
if (!page.IsReentrant &&
layerDict.Values.Any(p => string.Equals(p.Key, page.Key, StringComparison.Ordinal)))
{
Log.Warn("UI {0} is not reentrant but already exists in layer {1}", page.Key, layer);
throw new InvalidOperationException(

View File

@ -119,23 +119,19 @@ Godot 上。
### 4. 按需接入配置、存储和设置
当项目已经使用 `Game` family 的配置、存储、设置契约时,再补 Godot 侧实现:
当项目已经使用 `Game` 模块的配置、存储、设置契约时,再补 Godot 侧实现:
- 配置:`GodotYamlConfigLoader`
- 存储:`GodotFileStorage`
- 设置:`GodotAudioSettings``GodotGraphicsSettings``GodotLocalizationSettings`
不要把这些宿主实现误写成 `Game` family 的默认行为。
不要把这些宿主实现误写成 `Game` 模块的默认行为。
## `ai-libs/` 里的参考接入线索
`ai-libs/CoreGrid` 仍是当前最直接的消费者证据来源:
## 典型接入方式
- 架构侧保持普通模块注册,再按需挂接 Godot 宿主
- `project.godot` 元数据与节点样板交给 `GFramework.Godot.SourceGenerators`
- Scene / UI 继续沿用 `Game` family 的 router 语义
`ai-libs/` 与源码或测试冲突时,应以当前源码与测试为准。
- Scene / UI 继续沿用 `Game` 模块的路由语义
## 文档入口

View File

@ -0,0 +1,39 @@
# Analyzer Warning Reduction 跟踪归档RP074-RP078
## 范围
- 归档 `RP074``RP078` 期间从 active todo 中迁出的批次明细。
- 保留当前波次的已完成 slice 摘要、验证收口与延后候选,供后续恢复时回溯。
## 已完成批次摘要
- 第一轮并行 warning 清理:
- `GFramework.Core` 事件 / 状态 / 属性 / 协程统计中的 `MA0158` 专用锁迁移
- `GFramework.Game/Data``DataRepository``UnifiedSettingsDataRepository``SaveRepository``ConfigureAwait` / 比较器 / 专用锁修正
- `GFramework.Game/Scene/SceneRouterBase.cs``GFramework.Game/UI/UiRouterBase.cs` 中的显式上下文 / 参数名 / 比较器修正
- 收口提交:`fb0a55f` `fix(analyzer): 收口首轮并行警告清理`
- 第三轮 `Core.Tests` 低风险 slice
- `GFramework.Core.Tests/Concurrency/AsyncKeyLockManagerTests.cs``MA0004`
- `GFramework.Core.Tests/Pause/PauseStackManagerTests.cs``MA0158`
- `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs``MA0015`
- `GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs``MA0004`
## 批次验证快照
- `dotnet clean`
- 结果:提权直接执行成功,确认为当前权威 clean 基线
- `dotnet build`
- 结果提权直接构建成功warning 从 `639` 降到 `397`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:提权直接构建成功;`0 Warning(s)``0 Error(s)`
## 延后候选
- `GFramework.Game/Config/YamlConfigLoader.cs``MA0158`
- 原因:单点可修,但文件同时承载其他高耦合 warning不适合在当前低风险批次顺手推进
- 测试项目中的 `MA0048` 文件名拆分波次
- 原因:会显著增加 changed-file 数,更适合另开后续波次
## 关联资料
- 详细执行过程见 [analyzer-warning-reduction-history-rp073-rp078.md](../traces/analyzer-warning-reduction-history-rp073-rp078.md)。

View File

@ -0,0 +1,176 @@
# Analyzer Warning Reduction 追踪归档RP073-RP078
## 2026-04-27 — RP-078
### 阶段:完成第三轮 Core.Tests 低风险 slice 并在 30 files 处收口
- 触发背景:
- 第二轮结束后,`GFramework.Game` 低风险单文件 warning 已基本耗尽,继续推进更适合转向测试项目
- 第三轮选择的 `Core.Tests` slice 仍保持单文件、低耦合,且不会明显放大 branch diff
- 已接受的 delegated scope 与结果:
- worker-A`GFramework.Core.Tests/Concurrency/AsyncKeyLockManagerTests.cs`
- 结果:与 `PauseStackManagerTests.cs` 一并落在提交 `650618b`,修复该文件的 `MA0004`
- worker-B`GFramework.Core.Tests/Pause/PauseStackManagerTests.cs`
- 结果:与 `AsyncKeyLockManagerTests.cs` 一并落在提交 `650618b`,修复该文件的 `MA0158`
- worker-C`GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs``GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs`
- 结果:提交 `e19e60e`,修复 `MA0015` / `MA0004`
- 主线程验证里程碑:
- 提权 `dotnet clean`
- 结果:成功
- 提权 `dotnet build`
- 结果成功warning 从上一轮的 `405` 降到 `397`
- 提权 `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
- 结果:`30`
- `git diff --numstat refs/remotes/origin/main...HEAD`
- 结果:`642` changed lines
- 当前结论:
- 当前分支在 `30 / 50` files 时仍保持可审阅性,且已经连续三轮拿到了实质 warning 降幅
- 继续推进的剩余候选主要是 `YamlConfig*` 高耦合热点与 `MA0048` 批量拆分,不再符合本轮的低风险边界
- 默认建议在这里收口当前波次,把下一波次留给更明确的热点专项
## 2026-04-27 — RP-077
### 阶段:完成第二轮 Game 侧低风险 slice 验证并转向测试项目候选
- 触发背景:
- 第二轮 worker 已分别完成 `SettingsModel.cs``RouterBase.cs`+`UiInteractionProfiles.cs``GameConfigBootstrap.cs`
- 主线程在复验时发现 `SettingsModel.cs``GameConfigBootstrap.cs` 又各暴露一个 touched-file `MA0158`,已在主线程补齐
- 已接受的 delegated scope 与结果:
- worker-A`GFramework.Game/Setting/SettingsModel.cs`
- 结果:提交 `c106e53`,修复 `MA0004`;主线程随后补齐同文件 `MA0158`
- worker-B`GFramework.Game/Routing/RouterBase.cs``GFramework.Game/UI/UiInteractionProfiles.cs`
- 结果:提交 `9deafac`,修复 `MA0006` / `MA0099`
- worker-C`GFramework.Game/Config/GameConfigBootstrap.cs`
- 结果:提交 `9ce634e`,拆分热重载启动流程以修复 `MA0051`;主线程随后补齐同文件 `MA0158`
- explorer重新审视 `GFramework.Game` 排除热点后的剩余候选
- 结果:确认 `Game` 侧低风险单文件 warning 基本耗尽,继续推进应转向其他项目
- 主线程验证里程碑:
- 提权 `dotnet clean`
- 结果:成功
- 提权 `dotnet build`
- 结果成功warning 从上一轮的 `430` 继续降到 `405`
- 提权 `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果成功warning 从上一轮的 `147` 降到 `122`
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
- 结果:`26`
- `git diff --numstat refs/remotes/origin/main...HEAD`
- 结果:`483` changed lines
- 当前结论:
- 第二轮 Game 侧 warning 清理已完成验证,且 warning 数继续实质下降
- 当前分支距离 `$gframework-batch-boot 50` 仍有空间,但继续推进不应再硬碰 `YamlConfigSchemaValidator*` / `YamlConfigLoader.cs`
- 若继续下一轮,优先切向 `Core.Tests` 等测试项目里的单文件 `MA0004` / `MA0015` / `MA0158`
## 2026-04-27 — RP-076
### 阶段:首轮收口提交后进入第二轮低风险 Game warning slice
- 触发背景:
- 首轮并行清理已经以 `fb0a55f` 收口,当前分支相对 `origin/main` 的累计改动文件数来到 `22 / 50`
- 用户要求继续采用“先拿构建 warning再分批交给 subagent”模式因此当前仍有继续推进的 branch 预算
- 主线程当前真值:
- 当前基线:`refs/remotes/origin/main` = `617e0bf`
- 当前 `HEAD` stop metric
- files`22`
- changed lines`378`
- 最近权威验证仍为:
- `dotnet build``430 Warning(s)``0 Error(s)`
- `dotnet build GFramework.sln -c Release``147 Warning(s)``0 Error(s)`
- 本轮拟下发的 delegated scope
- worker-A`GFramework.Game/Setting/SettingsModel.cs`
- 目标:修复 `MA0004`,仅在不改变设置模型生命周期语义的前提下补全 `ConfigureAwait(false)`
- worker-B`GFramework.Game/Routing/RouterBase.cs``GFramework.Game/UI/UiInteractionProfiles.cs`
- 目标:修复 `MA0006` / `MA0099`,保持现有路由比较语义与 UI 动作位掩码语义不变
- worker-C`GFramework.Game/Config/GameConfigBootstrap.cs`
- 目标:评估并尽量修复 `MA0051`;若单文件安全提取不可低风险完成,应明确放弃并说明阻塞点
- 当前结论:
- 第二轮继续严格限制在低风险单文件 slice避免直接进入 `YamlConfigSchemaValidator*``YamlConfigLoader.cs` 这种高耦合热点
- 本轮完成后应重新评估 branch diff 是否仍适合继续在同一分支上批量推进
## 2026-04-27 — RP-075
### 阶段:完成 `$gframework-batch-boot 50` 第一轮并行 warning 清理集成
- 触发背景:
- 用户要求先以权威构建输出建立 warning 基线,再把低风险 warning family 按文件边界拆给不同 subagent 并行清理
- 当前批次已完成首轮 worker 集成,但第二组锁迁移、主线程补修与 `ai-plan` 同步仍在工作树,需先收口提交再进入下一轮
- 已接受的 delegated scope 与结果:
- worker-1`GFramework.Core` 事件 / 状态 / 属性 / 协程统计中的 `MA0158`
- 结果:已提交 `8f2d959`,采用 `#if NET9_0_OR_GREATER` + `System.Threading.Lock` / `object` 双分支兼容模式
- worker-2`GFramework.Core` / `GFramework.Cqrs` 资源、日志、配置缓存中的 `MA0158`
- 结果:改动已集成到工作树,待主线程与本轮 `ai-plan` 一并提交
- worker-3`GFramework.Game/Data``SceneRouterBase.cs`
- 结果:已提交 `e3eec54`,主线程随后补修 `SceneRouterBase.Contains``SaveRepository._migrationsLock` 的 touched-file 残留 warning
- worker-4`GFramework.Game/UI/UiRouterBase.cs`
- 结果:已提交 `7e13752`
- 主线程验证里程碑:
- 提权 `dotnet clean`
- 结果:成功
- 提权 `dotnet build`
- 结果成功warning 从本轮批次建立时的 `639` 降到 `430`
- 提权 `dotnet build GFramework.sln -c Release`
- 结果:成功;`147 Warning(s)``0 Error(s)`
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
- 结果:`12`
- `git diff --numstat refs/remotes/origin/main...HEAD`
- 结果:`192` changed lines
- 当前结论:
- 第一轮并行 warning 清理已经完成验证,且 warning 总量出现明显下降,可以继续按 batch 模式推进
- 当前 stop-condition 仍远低于 `$gframework-batch-boot 50`;但在派发下一轮之前,应该先提交当前工作树里的第二组锁迁移与恢复文档同步
- 下一轮优先目标保持“低风险、单文件、避免高耦合热点”,候选包括 `SettingsModel.cs``RouterBase.cs``UiInteractionProfiles.cs`
## 2026-04-27 — RP-074
### 阶段:按 `$gframework-batch-boot 50` 建立并行 warning 清理批次
- 触发背景:
- 用户明确要求在拿到构建 warning 后分批指派给不同 subagent以控制主线程上下文长度并提高 warning 清理效率
- 当前 worktree 映射到 `analyzer-warning-reduction` 主题,且该任务符合 batch candidate 条件:重复、可切片、可按文件边界独立验证
- 基线与停止条件:
- 当前基线采用 `refs/remotes/origin/main`
- `origin/main``HEAD` 当前同为 `617e0bf``2026-04-26T12:17:15+08:00`
- 主 stop condition 为 branch diff files 接近 `50`;当前为 `0 / 50`
- 主线程实施:
- 先读取 `AGENTS.md``.ai/environment/tools.ai.yaml``ai-plan/public/README.md` 以及当前 topic 的 active todo/trace确认批处理流程与 topic 上下文
- 先在沙箱内执行仓库根 `dotnet clean` / `dotnet build`;其中 `dotnet clean` 因缺失 Windows fallback package folder 失败,判定为环境噪音
- 按仓库规则提权重跑直接命令,确认权威基线为 `dotnet clean` 成功、`dotnet build` 成功且 `639 Warning(s)``0 Error(s)`
- 基于当前 warning 输出,预划分以下互不重叠的 subagent ownership
- `GFramework.Core` / `GFramework.Cqrs``MA0158` 专用锁迁移
- `GFramework.Game/Data``MA0004` 与局部 `MA0002`
- `GFramework.Game/Scene/SceneRouterBase.cs``GFramework.Game/UI/UiRouterBase.cs` 的显式上下文 / 参数名 / 比较器修正
- 验证里程碑:
- `dotnet clean`
- 结果:提权后成功;作为本轮 clean 真值
- `dotnet build`
- 结果:提权后成功;`639 Warning(s)``0 Error(s)`
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
- 结果:`0`
- `git diff --numstat refs/remotes/origin/main...HEAD`
- 结果:空输出
- 当前结论:
- 本轮已经完成 batch boot 所需的权威警告基线建立,可以安全进入并行 worker 阶段
- 当前优先级应继续保持在低风险、少文件、可独立验证的 warning family 上,不直接扩展到 `YamlConfigSchemaValidator` 这类高耦合热点
- 下一步默认由主线程下发 disjoint worker 任务并在集成后重新计算 branch diff 与 warning 结果
## 2026-04-26 — RP-073
### 阶段:脱敏 analyzer-warning-reduction 文档中的绝对路径记录
- 触发背景:
- 用户再次显式要求执行 `$gframework-pr-review`,当前分支仍对应 PR `#291`
- 最新抓取结果确认 latest-head 还剩 `2` 条 open review thread分别指向 active todo 与 archive trace 中记录的绝对路径
- active trace 当前也保留了同类 `/tmp` 路径记录;虽然这次 review 没直接点名,但继续保留会留下同一类治理缺口
- 主线程实施:
- 将 active todo 与 active trace 中的 PR review 输出路径改写为 `--json-output <current-pr-review-json>`
- 将 [analyzer-warning-reduction-history-rp062-rp071.md](analyzer-warning-reduction-history-rp062-rp071.md) 里的临时 `dotnet` home、PR review 输出路径和失效 Windows fallback package folder 改写为仓库安全占位符
- 同步刷新 active todo 中的 review 真值,把当前恢复点更新到 `RP-073`
- 验证里程碑:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
- 结果:成功;确认 PR `#291` latest-head open review thread 为 `2`,两者都指向 `ai-plan` 文档中的绝对路径记录
- `dotnet build`
- 结果:成功;`639 Warning(s)``0 Error(s)`;与当前权威仓库根基线一致
- 当前结论:
- 本轮只吸收当前仍成立的 PR review 文档项,不扩展到新的 warning 清理切片
- 当前仓库根 warning 权威基线仍保持 `639 Warning(s)``0 Error(s)`;本轮目标是让 analyzer-warning-reduction 主题下当前入口不再记录绝对路径
- 下一轮默认先推送本轮同步并重新执行 `$gframework-pr-review`,确认 PR `#291` 的 open thread 是否已自动收口

View File

@ -6,46 +6,54 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-073`
- 当前阶段:`Phase 73`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-084`
- 当前阶段:`Phase 84`
- 当前焦点:
- `2026-04-26` 主线程再次按 `$gframework-pr-review` 复核当前分支 PR `#291`,确认 latest-head 仍剩 `2` 条 open review thread均指向 `ai-plan` 文档中的绝对路径记录
- 当前批次同步 active todo/trace 与相关 archive trace把 PR review 输出路径、临时 `dotnet` home 和失效 Windows fallback package folder 改写为仓库安全占位符
- `dotnet clean` + `dotnet build` 的直接仓库根基线仍为 `639 Warning(s)``0 Error(s)`,因此本轮属于文档真值收口,而不是新的 warning 清理批次
- `2026-04-27` 已完成 PR `#297` 的 CodeRabbit follow-up修复 `YamlConfigLoader` 的取消语义与 `IntegerTryParseDelegate` 可空性问题
- 已补齐 `GFramework.Core.Tests/Ioc``GFramework.Core.Tests/Query` 中 review 指向的 XML 文档缺口,并让 `IPrioritizedService` 复用 `IMixedService.Name` 契约
- 已新增 `YamlConfigLoaderTests` 回归测试,锁定“取消时保留 `OperationCanceledException`”这一行为
- 当前分支的下一波 warning reduction 仍建议回到 `ArchitectureContextTests.cs``AsyncQueryExecutorTests.cs``YamlConfigSchemaValidator*` 的后续 slice
## 当前活跃事实
- 当前 `origin/main` 基线提交为 `4ad880c``2026-04-25T14:35:38+08:00`)。
- 提权后的直接仓库根验证当前确认为:
- `dotnet clean`
- 结果:成功;此前沙箱内 “Build FAILED but 0 errors” 的 clean 结果不是仓库真值
- `dotnet build`
- 最新结果:成功;`639 Warning(s)``0 Error(s)`
- 当前分支低风险批次文件:
- `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md`
- `ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md`
- `ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp062-rp071.md`
- 当前批次验证结果:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
- 最新主线程结果:成功;确认 PR `#291` latest-head open review thread 为 `2`,两者都指向 `ai-plan` 文档中的绝对路径记录
- `dotnet build`
- 最新主线程结果:成功;`639 Warning(s)``0 Error(s)`;与当前权威仓库根基线一致
- 当前 `origin/main` 基线提交为 `b6a9fef``2026-04-27T10:53:34+08:00`)。
- 当前直接验证结果:
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 最新结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 最新结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests.ReadYamlAsync_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested"`
- 最新结果:成功;`1` 通过、`0` 失败
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests.GetAllByPriority_Should_Sort_By_Priority_Ascending"`
- 最新结果:成功;`1` 通过、`0` 失败
- `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigLoader.cs GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs GFramework.Core.Tests/Ioc/IMixedService.cs GFramework.Core.Tests/Ioc/IPrioritizedService.cs GFramework.Core.Tests/Ioc/PrioritizedService.cs GFramework.Core.Tests/Query/TestAsyncQueryWithExceptionV4.cs`
- 最新结果:成功;本次 PR follow-up 改动文件无需额外格式化
- 当前批次摘要:
- 本轮完成 PR `#297` 最新 head review 中仍然有效的 `3` 个 open threads 修复:`YamlConfigLoader` 取消语义、`IMixedService.Name` XML 文档、`IPrioritizedService` 相关契约整理
- 本轮同时吸收 CodeRabbit folded nitpick 中仍然成立的 `2` 个点:`IntegerTryParseDelegate` 可空性对齐、`TestAsyncQueryWithExceptionV4.OnDoAsync``<returns>` 文档
- 本轮新增一条精确回归测试,确保底层 YAML 文件读取在取消时继续抛出 `OperationCanceledException` 系列异常,而不是包装成 `ConfigLoadException`
- 当前建议保留到下一波次的候选:
- `GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs``7``MA0048`
- `GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs``7``MA0048`
- `GFramework.Game/Config/YamlConfigSchemaValidator.cs``YamlConfigSchemaValidator.ObjectKeywords.cs` 的高耦合 warning 热点
## 当前风险
- `GFramework.Core``GFramework.Game``GFramework.Core.Tests``GFramework.Cqrs.Tests` 仍有较大 warning 基线。
- 缓解措施:后续批次继续优先挑低风险、少文件、可独立验证的测试与局部逻辑切片。
- 当前 review 相关真值要等新 head 推送后才能在 GitHub UI 中自动收口
- 缓解措施:本轮提交后立即重新执行 `$gframework-pr-review`,确认 PR `#291` 的 latest-head thread 与 nitpick 是否消失
- `GFramework.Cqrs.Tests/Mediator/*` 仍有 `47` / `44` / `34` 个唯一 warning 位点,属于高 changed-file 风险的 `MA0048` 大波次
- 缓解措施:优先继续处理 `6-7` 个 warning 的小文件切片,避免一次性推高文件数
- `YamlConfigSchemaValidator*` 仍然聚集多类高耦合 warning
- 缓解措施:继续把它们留在独立波次,不与测试项目的低风险拆分混提
## 活跃文档
- 当前轮次归档:
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- 历史跟踪归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
- 历史 trace 归档:
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
@ -53,11 +61,12 @@
## 验证说明
- 权威验证结果统一维护在“当前活跃事实”和“当前批次验证结果”。
- 后续若刷新构建或 PR review 真值,只更新上述权威区块,不在本节重复抄录。
- 权威验证结果统一维护在“当前活跃事实”。
- `GFramework.Core.Tests` 项目级 Release 构建已在本轮清零,但仓库根 non-incremental 构建仍保留大量既有 warning。
- warning reduction 的仓库级真值只以同轮 `dotnet clean` 后的 `dotnet build` 为准。
## 下一步建议
1. 推送包含本轮 absolute-path 脱敏的提交后,重新执行 `$gframework-pr-review`,确认 PR `#291` 的 latest-head open thread 是否已自动收口
2. 若 PR `#291` 已清零,继续以当前 `639 Warning(s)` 根基线为恢复点,按 `$gframework-batch-boot 50` 规则挑选下一个 1-3 文件的低风险热点
3. 若 GitHub 仍保留 review 信号,先确认它们是否仍指向新 head再决定是否需要继续清理同主题下的其它历史 `ai-plan` 记录
1. 提交本轮 PR `#297` review follow-up 与 `ai-plan` 同步
2. 下一波优先挑选 `ArchitectureContextTests.cs``AsyncQueryExecutorTests.cs` 这类 `7`-warning 的纯 `MA0048` 单文件切片
3. 继续将 `YamlConfigSchemaValidator*``GFramework.Cqrs.Tests/Mediator/*` 作为独立高风险波次处理

View File

@ -1,32 +1,87 @@
# Analyzer Warning Reduction 追踪
## 2026-04-26 — RP-073
## 2026-04-27 — RP-084
### 阶段:脱敏 analyzer-warning-reduction 文档中的绝对路径记录
### 阶段:收敛 PR #297 的 CodeRabbit follow-up
- 触发背景:
- 用户再次显式要求执行 `$gframework-pr-review`,当前分支仍对应 PR `#291`
- 最新抓取结果确认 latest-head 还剩 `2` 条 open review thread分别指向 active todo 与 archive trace 中记录的绝对路径
- active trace 当前也保留了同类 `/tmp` 路径记录;虽然这次 review 没直接点名,但继续保留会留下同一类治理缺口
- 用户执行 `$gframework-pr-review`,要求以当前分支对应 PR 为准,提取并核对 AI review / check 信号
- `fetch_current_pr_review.py` 返回 PR `#297` 的最新 head review 中仍有 `3` 个 open threads另有 `2` 个 folded nitpick 仍然适用
- 主线程实施:
- 将 active todo 与 active trace 中的 PR review 输出路径改写为 `--json-output <current-pr-review-json>`
- 将 [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md) 里的临时 `dotnet` home、PR review 输出路径和失效 Windows fallback package folder 改写为仓库安全占位符
- 同步刷新 active todo 中的 review 真值,把当前恢复点更新到 `RP-073`
- 校验 `GFramework.Game/Config/YamlConfigLoader.cs` 后,保留 `ReadYamlAsync` 的原始取消语义,并把 `IntegerTryParseDelegate<T>` 第一个参数改为 `string?`
- 校验 `GFramework.Core.Tests/Ioc/*``Query/TestAsyncQueryWithExceptionV4.cs` 后,补齐缺失 XML 文档,让 `IPrioritizedService` 继承 `IMixedService` 复用 `Name` 契约,并补上 `<returns>` 文档
- 新增 `YamlConfigLoaderTests.ReadYamlAsync_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested`,用反射直接命中私有读取路径,稳定回归本次取消语义修复
- 用 `dotnet format --verify-no-changes --include ...` 清理并验证本次改动文件的格式状态
- 验证里程碑:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
- 结果:成功;确认 PR `#291` latest-head open review thread 为 `2`,两者都指向 `ai-plan` 文档中的绝对路径记录
- `dotnet build`
- 结果:成功;`639 Warning(s)``0 Error(s)`;与当前权威仓库根基线一致
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests.ReadYamlAsync_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested"`
- 结果:成功;`1` 通过、`0` 失败
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests.GetAllByPriority_Should_Sort_By_Priority_Ascending"`
- 结果:成功;`1` 通过、`0` 失败
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigLoader.cs GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs GFramework.Core.Tests/Ioc/IMixedService.cs GFramework.Core.Tests/Ioc/IPrioritizedService.cs GFramework.Core.Tests/Ioc/PrioritizedService.cs GFramework.Core.Tests/Query/TestAsyncQueryWithExceptionV4.cs`
- 结果:成功
- 当前结论:
- 本轮只吸收当前仍成立的 PR review 文档项,不扩展到新的 warning 清理切片
- 当前仓库根 warning 权威基线仍保持 `639 Warning(s)``0 Error(s)`;本轮目标是让 analyzer-warning-reduction 主题下当前入口不再记录绝对路径
- 下一轮默认先推送本轮同步并重新执行 `$gframework-pr-review`,确认 PR `#291` 的 open thread 是否已自动收口
- PR `#297` 当前仍然有效的 CodeRabbit open threads 与 folded nitpick 已在本地全部核对并收敛
- 当前恢复点完成后,分支可以回到 `ArchitectureContextTests.cs` / `AsyncQueryExecutorTests.cs` / `YamlConfigSchemaValidator*` 的 warning reduction 主线
- 下一步:
1. 提交本轮 PR review follow-up。
2. 继续执行下一波 `MA0048` 小切片,优先避免一次性进入 `Mediator*` 的高 changed-file 风险波次。
## 2026-04-27 — RP-083
### 阶段:修复 `YamlConfigLoader` 单文件 warning并拆分 `MicrosoftDiContainerTests` 的辅助类型
- 触发背景:
- 用户执行 `$gframework-batch-boot 50`,要求先拿仓库根构建 warning再按 bounded slice 分派给不同 subagent 并持续推进
- 当前分支在本轮开始时与 `origin/main@b6a9fef` 零提交差异,适合从低风险 warning slice 起步
- 主线程实施:
- 先执行 non-incremental 仓库根基线:`dotnet clean` + `dotnet build`,得到 `397 Warning(s)` / `316` 个唯一位点
- 主线程修复 `GFramework.Game/Config/YamlConfigLoader.cs``MA0051``MA0002``MA0158`
- 接受一个 worker batch`GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 末尾的 `10` 个测试辅助接口/类拆分到 `Ioc/` 同目录独立文件
- 接受第二波 worker 的已落地结果:将 `GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs` 末尾的 `7` 个测试辅助类型拆分到 `Query/` 同目录独立文件
- 启动 `ArchitectureContextTests.cs` 候选 worker但在共享工作树落地前主动停止以避免本轮上下文与 review 面积继续膨胀
- 验证里程碑:
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`111 Warning(s)``0 Error(s)`
- 观察:构建输出未再报告 `GFramework.Game/Config/YamlConfigLoader.cs`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet clean`
- 结果:成功;刷新最终 non-incremental 仓库根 warning 基线
- `dotnet build`
- 结果:成功;`353 Warning(s)``0 Error(s)`,唯一位点 `279`
- 观察:构建输出未再报告 `GFramework.Game/Config/YamlConfigLoader.cs``GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs``GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs`
- 当前结论:
- 本轮已完成一个主线程单文件 slice 和两个 worker 拆分 slice仓库根 non-incremental warning 从 `397` 降到 `353`
- 当前共享工作树 footprint 为 `22` 个 changed files仍低于 `$gframework-batch-boot 50` 的停止线
- 下一波更适合继续处理 `7``MA0048` 的小文件,而不是立即进入 `Mediator*``YamlConfigSchemaValidator*` 的高耦合热点
## 活跃风险
- `GFramework.Cqrs.Tests/Mediator/*``MA0048` 位点密度很高,一次性拆分会迅速推高 changed-file 数。
- 缓解措施:下一波优先继续拿 `7` warning 级别的小切片。
- `YamlConfigSchemaValidator*` 仍然聚集多类高耦合 warning。
- 缓解措施:继续维持为独立波次,不与测试项目拆分混提。
## 下一步
1. 完成本轮 `YamlConfigLoader.cs``MicrosoftDiContainerTests.cs``ai-plan` 的提交。
2. 下一波优先从 `ArchitectureContextTests.cs``AsyncQueryExecutorTests.cs` 继续拆分纯 `MA0048`
## 历史归档指针
- 最新 trace 归档:
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
- 早期 trace 归档:
- 历史 todo 归档:
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- 早期归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)

View File

@ -12,14 +12,13 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-040`
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-044`
- 当前阶段:`Phase 5 - Governance Maintenance`
- 当前焦点:
- 继续以最新 `origin/main``79934f7``2026-04-25 16:15:55 +08:00`)作为 baseline当前批处理 stop condition 仍是 branch diff vs baseline 接近 `50` changed files
- 当前批次已从“单点 review 收口”切到“覆盖整个项目功能的 reader-facing 文档补齐”,重点处理 4 组低风险切片meta-package / 安装入口、config tool adoption、source-generators 真实契约修正、内部支撑模块 README
- 当前未提交工作树已触达 `18` 个文件,其中 `14` 个更新、`4` 个新增;当前 committed branch diff vs `origin/main` 仍为 `3` files提交本批次后仍明显低于 `50` 文件阈值
- 已接受 subagent 结论:`Cqrs` 当前不是栏目缺失,而是 `docs/zh-CN/core/cqrs.md` 需要补 `Request` / stream 变体与协程入口source-generators 侧当前优先修正文档失真与共享支撑层说明,而不是扩新导航
- `2026-04-26` 重新抓取 PR `#292` 后确认 latest reviewed commit 已推进到 `d3d62cf4541063c46458f88eea0f5acd1b4503f9`;当前 open thread 仍集中在 `tools/gframework-config-tool/README.md`,其中“缺少中文文档入口链接”已在本地工作树验证为过期评论,仍需收口的是补最小接入路径以及统一 `current schema subset` 术语
- 继续以最新 `origin/main``617e0bf``2026-04-26 12:17:15 +08:00`)作为 baseline当前批处理 stop condition 仍是 branch diff vs baseline 接近 `50` changed files
- 本轮从 `$gframework-pr-review` 重新抓取当前 PR `#296`,确认 latest reviewed commit 为 `5778782df05e22dd24dc95189dd768458afb8537`,剩余 open thread 都落在 reader-facing 文案与 README 导航收口上
- 当前工作树相对 `origin/main` 的 tracked diff 仍接近 `50` files因此本轮只接受 latest-head review 中仍成立的 4 条低风险修正,不再扩新栏目或新专题页
- 已确认 `Title check` 的 inconclusive 仅是 GitHub PR 标题元数据提示,不属于仓库文件内可修复范围;本轮只处理本地仍成立的文档线程
## 当前状态摘要
@ -32,6 +31,14 @@
- `2026-04-25` `docs/zh-CN/source-generators/index.md` 已按 PR `#292` review 调整“共享支撑模块”段落句式,避免“对读者更重要的判断是”这类拗口表达。
- `2026-04-25` `tools/gframework-config-tool/README.md` 已新增 `Documentation` 章节,直接链接到 `docs/zh-CN/game/config-tool.md``config-system.md`,让工具 README 能回到完整中文接入文档。
- `2026-04-26` `tools/gframework-config-tool/README.md` 已补 `Quick Start`,把安装扩展、配置 `configPath` / `schemasPath`、打开 Explorer、先跑校验、再进入表单 / 批量编辑的最小接入路径串起来,并把 `Validation Coverage``stable config-schema subset` 统一为 `current schema subset`
- `2026-04-27` `docs/zh-CN/getting-started/installation.md` 已补齐当前公开选包矩阵,新增 `Core.Abstractions``Game.Abstractions``Ecs.Arch``Ecs.Arch.Abstractions` 的 reader-facing 安装说明,并把 `Godot` 常见问题里的旧版 `>= 4.5` 提示收敛到当前 `4.6.2` 基线。
- `2026-04-27` `GFramework.Core.Abstractions/README.md``GFramework.Game.Abstractions/README.md``GFramework.Game.SourceGenerators/README.md``GFramework.Ecs.Arch.Abstractions/README.md` 当前都已把 XML 阅读入口改写为“代表类型 + 阅读重点”,不再暴露覆盖计数、日期或 `已覆盖` 这类治理字段。
- `2026-04-27` `docs/zh-CN/game/config-system.md``docs/zh-CN/tutorials/basic/index.md` 已把维护者 / 指挥式措辞改成中性的采用建议与阅读入口,避免公开页面继续暴露内部决策口吻。
- `2026-04-27` `docs/zh-CN/getting-started/index.md``core/index.md``game/index.md``api-reference/index.md``source-generators/index.md` 已统一收敛为“适用场景 / 起步路线 / 继续阅读”式 reader-facing 入口,不再把 GitHub blob README 或治理说明当作主导航。
- `2026-04-27` `GFramework.Game/README.md``GFramework.Game.Abstractions/README.md``GFramework.Godot/README.md``GFramework.Cqrs.Abstractions/README.md``GFramework.Ecs.Arch/README.md` 已收口 `ai-libs``family``seam``ReadMe.md` 等内部化或文件名式表述。
- `2026-04-27` `docs/zh-CN` 当前已清空所有指向 `github.com/GeWuYou/GFramework/blob/main/.../README.md` 的公开外链,相关入口统一回到站内栏目页、专题页或 API 导航。
- `2026-04-27` `docs/zh-CN/tutorials/godot-integration.md``game/setting.md``game/serialization.md``godot/index.md``godot/architecture.md``godot/storage.md``godot/logging.md``godot/setting.md``godot/extensions.md``core/architecture.md` 已把 `旧文档` / `ai-libs` / `.Wait()` / `family` 这类维护与内部语气改写成当前采用说明。
- `2026-04-27` 已重新抓取 PR `#296` 并逐条复核 latest-head review`GFramework.Game.SourceGenerators/README.md` 的 XML 阅读表已改成语义标签,`GFramework.Game/README.md` 已删除重复的 `storage.md` 入口,`docs/zh-CN/tutorials/godot-integration.md``docs/zh-CN/godot/extensions.md` 已收口仍成立的 reader-facing 措辞问题。
- `2026-04-25` 当前批次已补齐 meta-package / 安装面:`GFramework.csproj` 不再保留占位描述,`README.md``docs/zh-CN/index.md``docs/zh-CN/getting-started/installation.md` 当前明确说明聚合元包只聚合 `Core` + `Game`,并把安装入口更新到当前 `net8.0/net9.0/net10.0` 与 Godot `4.6.2` 基线。
- `2026-04-25` `docs/zh-CN/game/config-tool.md` 已新增为 reader-facing 工具页,`docs/zh-CN/game/index.md``config-system.md``docs/.vitepress/config.mts``tools/gframework-config-tool/README.md` 当前把 VS Code 配置工具纳入 `Game` 配置工作流入口。
- `2026-04-25` source-generators 栏目已修正 4 处真实契约问题:`GetNode` 显式路径 / `Lookup` 语义、枚举生成器实际开关、`Context Get` 集合注入边界,以及 `GFramework.SourceGenerators.Common` / `*.SourceGenerators.Abstractions` 的共享支撑层说明。
@ -51,7 +58,7 @@
`MSB4276` / `MSB4018`;这是已知环境阻塞,不属于本轮文档回归。
- 当前 WSL 会话里 `git.exe` 可解析但不能执行,应继续使用显式 `--git-dir` / `--work-tree` 绑定作为默认 Git 策略。
- `dotnet build GFramework.csproj -c Release` 当前仍会输出仓库既有 analyzer warnings`MA0158``MA0051``MA0004`);本轮仅修改文档与 package metadata不扩展到 warning 清理。
- PR `#292` 当前 review 线程仍主要来自 CodeRabbit对 reader-facing 文案和文档入口连通性要求较细;本轮提交后仍需重新抓取 latest-head review确认 open thread 是否已自动关闭。
- PR `#296` 当前 review 线程仍主要来自 CodeRabbit 与 Greptile,对 reader-facing 文案和文档入口连通性要求较细;本轮提交后仍需重新抓取 latest-head review确认 open thread 是否已自动关闭。
## 归档指针
@ -68,12 +75,82 @@
## 最新验证
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game.SourceGenerators/README.md GFramework.Game/README.md`
- 结果:通过;本轮 2 个 README 的 reader-facing 表格与导航去重调整后链接目标有效。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
- 结果通过Godot 集成教程的措辞收口后页面 frontmatter、链接与代码块校验均通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
- 结果通过Godot 扩展页去自我指涉表述后页面 frontmatter、链接与代码块校验均通过。
- `2026-04-27` `bun run build`(工作目录:`docs/`
- 结果:通过;本轮 PR `#296` review 收口后的站点仍可构建,仅保留既有大 chunk warning。
- `2026-04-27` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果通过PR `#296` 处于 `OPEN`latest head review 共有 `4` 条 open thread其中 `3` 条文档问题与 `1` 条措辞 nitpick 在本地复核后仍成立;测试汇总为 `2156 passed`,仅剩 `Title check` inconclusive。
- `2026-04-25` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
- 结果通过PR `#290` 处于 `OPEN`latest head commit `54b8e5770af9ab3c8a86a396ffa4794fe4bb5181``2` 条 open threadCodeRabbit `1`、Greptile `1`),测试汇总为 `2156 passed`,无 failed checks。
- `2026-04-25` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
- 结果通过PR `#292` 处于 `OPEN`latest head commit `b96565ffa367bade30f44c2d4e8955143fbff85e``2` 条 CodeRabbit open thread测试汇总为 `2156 passed`,无 failed tests另有 `Title check` inconclusive属于 PR 标题元数据问题,不是仓库文件阻塞。
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core/README.md GFramework.Ecs.Arch/README.md GFramework.Game/README.md`
- 结果:通过;本轮 3 个模块 README 调整后链接目标仍然有效。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core.Abstractions/README.md GFramework.Game.Abstractions/README.md GFramework.Game.SourceGenerators/README.md GFramework.Ecs.Arch.Abstractions/README.md`
- 结果通过4 个公开模块 README 的 reader-facing 改写后链接目标仍然有效。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/getting-started`
- 结果:通过;`installation.md` 更新后 `getting-started` 栏目的 frontmatter、链接与代码块校验均通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/config-system.md`
- 结果:通过;`config-system.md` 的工具形态建议改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/basic`
- 结果:通过;基础教程入口的阅读路径改写后栏目校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game/README.md GFramework.Game.Abstractions/README.md GFramework.Godot/README.md GFramework.Cqrs.Abstractions/README.md GFramework.Ecs.Arch/README.md`
- 结果:通过;本轮 5 个模块 README 的 reader-facing 术语与入口改写后链接目标有效。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/index.md`
- 结果:通过;教程页受众表述改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
- 结果通过Godot UI 页的接法示例与 reader-facing 术语改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
- 结果通过Godot 场景页的接法示例与 reader-facing 术语改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
- 结果:通过;信号页切回站内生成器入口后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions`
- 结果通过3 个抽象层页改回站内入口后栏目校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators`
- 结果:通过;生成器栏目及受影响专题页改回站内入口后栏目校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
- 结果通过Core 入口页 reader-facing 改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/index.md`
- 结果通过Game 入口页 reader-facing 改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`
- 结果通过API 入口页导航改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/getting-started/quick-start.md`
- 结果:通过;快速开始页切回站内安装入口后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
- 结果通过CQRS 页继续阅读入口改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/scene.md`
- 结果通过Game 场景页相关推荐改回站内入口后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/ui.md`
- 结果通过Game UI 页相关推荐改回站内入口后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
- 结果通过ECS Arch 页入口改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
- 结果通过Godot 集成教程的接线口吻改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/setting.md`
- 结果:通过;设置系统页初始化语义改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/serialization.md`
- 结果:通过;序列化页生命周期说明改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
- 结果通过Godot landing page 的采用说明改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
- 结果通过Godot 架构页异步初始化口吻改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`
- 结果通过Godot 存储页示例口吻改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
- 结果通过Godot 日志页 provider 接线说明改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`
- 结果通过Godot 设置页 applicator 接线口吻改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
- 结果通过Godot 扩展页边界说明改写后页面校验通过。
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/architecture.md`
- 结果通过Core 架构页旧初始化入口改写后页面校验通过。
- `2026-04-27` `bun run build`(工作目录:`docs/`
- 结果:通过;本轮 README、安装页与公开文案改写后站点仍可构建仅保留既有大 chunk warning。
- `2026-04-25` `bun run build`(工作目录:`docs/`
- 结果:通过;移除 `api-reference` 侧栏重复项并统一 `source-generators` 标签后站点仍可正常构建,仅保留既有大 chunk warning。
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh README.md GFramework.Core/README.md GFramework.Core.Abstractions/README.md GFramework.Game/README.md GFramework.Game.Abstractions/README.md GFramework.Game.SourceGenerators/README.md GFramework.Ecs.Arch/README.md GFramework.Ecs.Arch.Abstractions/README.md`
@ -160,11 +237,9 @@
## 下一步
1. 运行 `bun run test`(工作目录:`tools/gframework-config-tool/`),完成本轮 README 收口改动的最小模块验证。
2. 验证通过后重新抓取 `$gframework-pr-review`,确认 PR `#292` 的 latest-head review 是否只剩过期线程或已自动清空。
3. 若本轮提交后 branch diff vs `origin/main` 仍明显低于 `50` 文件阈值,再决定是否继续追加低风险 reader-facing 文档切片。
4. 若后续继续扩批,优先在已识别但尚未扩写的低风险 reader-facing 方向里选择下一组config tool 更深的 adoption 示例、首页 / 安装页的进一步选包引导,或其它 repo-visible support surface 的本地说明补齐。
5. 若后续分支继续调整 `Game` persistence runtime、README 或公共 API优先复核 `docs/zh-CN/game/data.md`
1. 提交当前接近阈值的稳定批次后,优先重新抓取 `$gframework-pr-review` 或在新一轮里按 `46 / 50` 的 branch diff 重新评估是否还适合继续扩批。
2. 若后续还要继续文档治理,优先复核尚未触达的 `Game` persistence、Godot runtime 细页与少量残余 `ai-libs` 口吻,而不是继续扩大同一轮 review 面。
3. 若后续分支继续调整 `Game` persistence runtime、README 或公共 API优先复核 `docs/zh-CN/game/data.md`
`storage.md``serialization.md``setting.md` 与 landing page 是否仍保持同一套职责边界。
6. 若后续分支继续调整 `Godot` generator 接法,优先复核 `GFramework.Godot.SourceGenerators/README.md`
4. 若后续分支继续调整 `Godot` generator 接法,优先复核 `GFramework.Godot.SourceGenerators/README.md`
`docs/zh-CN/tutorials/godot-integration.md` 与相关专题页是否仍保持一致。

View File

@ -1,5 +1,138 @@
# Documentation Full Coverage Governance Trace
## 2026-04-27
### 当前恢复点RP-044
- 本轮从 `$gframework-pr-review` 重新进入,继续沿用显式 `--git-dir` / `--work-tree` 绑定确认当前分支仍为 `docs/sdk-update-documentation`,并通过 `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 抓取当前 PR `#296`
- 抓取结果显示 latest reviewed commit 为 `5778782df05e22dd24dc95189dd768458afb8537`,共有 `4` 条 open thread`GFramework.Game.SourceGenerators/README.md` 的表头仍带路径视角、`GFramework.Game/README.md` 有重复 `storage.md` 链接、`docs/zh-CN/tutorials/godot-integration.md``docs/zh-CN/godot/extensions.md` 还有 reader-facing 措辞收口空间。
- 本地逐条复核后确认这 `4` 条都仍成立,但都属于低风险文档收口;唯一 failed check `Title check` 只是 PR 标题元数据提示,不属于仓库文件内修复范围。
### 当前决策RP-044
- 接受 latest-head review 中仍成立的 `4` 条文档修正,不扩展到 review 未指向的其它页面,避免在当前接近 branch-size stop condition 的阶段继续增大 review 面。
- 对 README 表格和导航问题,只做 reader-facing 命名与去重;对教程与 Godot 页面,只做措辞收口,不改变现有采用路径与示例结构。
- 在同一轮里同步更新 active topic tracking / trace并在提交前运行最小页面校验、README 链接校验与站点构建。
### 当前验证RP-044
- PR review 抓取:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果通过PR `#296` 处于 `OPEN`latest head review 共有 `4` 条 open thread测试汇总为 `2156 passed`,仅剩 `Title check` inconclusive。
- README 链接校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game.SourceGenerators/README.md GFramework.Game/README.md`
- 结果:通过;本轮 2 个 README 的 reader-facing 表格与导航去重调整后链接目标有效。
- 页面校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
- 结果:通过;两页 frontmatter、链接与代码块校验均通过。
- 站点构建:
- `bun run build`(工作目录:`docs/`
- 结果:通过;本轮 PR `#296` review 收口后的站点仍可构建,仅保留既有大 chunk warning。
### 当前恢复点RP-043
- 在提交 `docs(reader-facing): 统一站内入口与公开术语` 后重新计算 branch diff确认当前工作树继续补一批新文件后已到 `46` changed files已经接近 `$gframework-batch-boot 50` 的停止线。
- 因此本轮最后只接受 10 个还没进入 branch diff 的文件:`tutorials/godot-integration.md``game/setting.md``game/serialization.md``godot/index.md``godot/architecture.md``godot/storage.md``godot/logging.md``godot/setting.md``godot/extensions.md``core/architecture.md`
- 这批文件统一收口的是同一类问题:把 `旧文档``ai-libs``.Wait()``family` 之类维护 / 内部口吻改成当前采用指导,不扩新结构、不重写示例体系。
### 当前决策RP-043
- 当前 stop condition 已接近阈值,因此这批验证通过后立即停止继续扩批,避免 branch diff 超过 `50` files 或让 review 面退化。
- 提交后本轮默认结束;后续若继续,应从 PR review 或剩余未触达的细页重新开一轮,而不是在同一轮里继续堆文件数。
### 当前验证RP-043
- 单页校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/setting.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/godot/index.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
- `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/logging.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/architecture.md`
- 结果:通过;本轮 10 个新文件的 frontmatter、链接与代码块校验全部通过。
- 站点构建:
- `bun run build`(工作目录:`docs/`
- 结果:通过;接近阈值前的最后一批文案收口后站点仍可构建,仅保留既有大 chunk warning。
### 当前恢复点RP-042
- 用户明确要求在当前阈值内循环推进,并允许使用 subagent 降低主线程上下文压力;因此本轮在主线程保留实现与验证,把热点识别委派给 3 个 explorer。
- 接受的 subagent 结论主要有三类:
- 入口页最划算的改法是统一 reader-facing 骨架,而不是继续保留治理说明或负向 framing。
- 若站内已有栏目页与专题页GitHub blob README 不应继续作为公开文档主导航。
- `GFramework.Game` / `Game.Abstractions` / `Godot` 等 README 仍有 `ai-libs``family``seam``ReadMe.md` 等对外不友好的措辞,适合在同一轮里收口。
- 基于这些结论,本轮连续落地 3 组低风险切片:入口页 reader-facing 改写、README / Godot 页去内部口吻、剩余 GitHub blob README 外链改回站内入口。
### 当前决策RP-042
- 继续保持 critical path 本地执行,不让 subagent 直接改文件subagent 只负责热点排序与问题归类。
- stop condition 继续沿用 `origin/main` + `50` changed files当前工作树相对 baseline 的 tracked diff 已到 `36` files / `500` changed lines意味着还能再做一小批但应先提交当前稳定批次。
- 当前批次不扩展到新栏目、新导航层或大段内容重写,只做 reader-facing 入口、术语和站内导航连通性收口。
### 当前验证RP-042
- README 链接校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game/README.md GFramework.Game.Abstractions/README.md GFramework.Godot/README.md GFramework.Cqrs.Abstractions/README.md GFramework.Ecs.Arch/README.md`
- 结果:通过;本轮 5 个 README 的 reader-facing 改写后链接目标有效。
- 教程 / Godot 页面校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/index.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
- 结果:通过;受影响页面的 frontmatter、链接与代码块校验通过。
- 入口与专题页校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/index.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/getting-started/quick-start.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/scene.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/ui.md`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
- 结果:通过;入口页和相关推荐入口改写后页面校验通过。
- 栏目级校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions`
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators`
- 结果:通过;抽象层与生成器栏目改回站内入口后栏目校验通过。
- 站点构建:
- `bun run build`(工作目录:`docs/`
- 结果:通过;本轮 3 组 reader-facing 文档批次后站点仍可构建,仅保留既有大 chunk warning。
### 当前恢复点RP-041
- 通过 `$gframework-batch-boot 50` 重新进入后,先按仓库规则读取 `AGENTS.md``.ai/environment/tools.ai.yaml``ai-plan/public/README.md` 与 active topic tracking / trace并继续使用显式 `--git-dir` / `--work-tree` 绑定确认当前分支仍为 `docs/sdk-update-documentation`
- 使用显式 Git 绑定确认最新 baseline 为 `origin/main` `617e0bf``2026-04-26 12:17:15 +08:00`),当前 committed branch diff vs baseline 为 `0` files因此本轮继续选择低风险、reader-facing 文档切片。
- 本轮收敛出的 3 组切片分别是:`installation.md` 的选包矩阵与旧版 Godot 提示、公开 README 的 XML 阅读入口去治理化,以及 `config-system` / 基础教程入口中的维护者口吻改写。
### 当前决策RP-041
- 不扩展到导航结构或新专题页,只在现有入口上修正 reader-facing 采用路径与表述一致性。
- 对公开 README 中的 XML 阅读入口,统一改成“代表类型 + 阅读重点”,不再暴露覆盖计数、日期或 `已覆盖` 这类治理字段。
- stop condition 继续沿用 `origin/main` + `50` changed files本轮工作树相对 baseline 的 tracked diff 为 `9` files / `191` changed lines仍明显低于阈值。
### 当前验证RP-041
- README 链接校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core.Abstractions/README.md GFramework.Game.Abstractions/README.md GFramework.Game.SourceGenerators/README.md GFramework.Ecs.Arch.Abstractions/README.md`
- 结果:通过;本轮 4 个 README 的 reader-facing 改写后链接目标有效。
- 入门栏目校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/getting-started`
- 结果:通过;`installation.md` 更新后 `getting-started` 栏目 frontmatter、链接与代码块校验通过。
- 配置系统页校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/config-system.md`
- 结果:通过;工具形态建议改写后页面校验通过。
- 基础教程栏目校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/basic`
- 结果:通过;入口页阅读路径改写后栏目校验通过。
- 站点构建:
- `bun run build`(工作目录:`docs/`
- 结果:通过;本轮文档批次后站点仍可构建,仅保留既有大 chunk warning。
## 2026-04-26
### 当前恢复点RP-040

View File

@ -97,7 +97,7 @@ public sealed class DiagnosticsFeature
1. 先读本页,确认你是否真的只需要契约层
2. 再看 [Core 模块总览](../core/index.md) 了解默认运行时怎么组织这些契约
3. 回到模块 README
- [Core 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
3. 再按需要回到:
- [入门指南](../getting-started/index.md)
- [Core 模块总览](../core/index.md)
4. 需要统一导航时,再看 [API 参考](../api-reference/index.md)

View File

@ -89,6 +89,6 @@ var options = new ArchOptions
1. 先读本页,确认你是否真的只需要契约层
2. 如果需要默认实现,再看 [Arch ECS 集成](../ecs/arch.md)
3. 回到对应模块 README
- [ECS 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md)
- [Ecs.Arch 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)
3. 需要统一入口时,再看
- [ECS 模块总览](../ecs/index.md)
- [入门指南](../getting-started/index.md)

View File

@ -116,6 +116,6 @@ public sealed class ContinueGameCommandHandler
- [设置系统](../game/setting.md)
- [场景系统](../game/scene.md)
- [UI 系统](../game/ui.md)
4. 需要仓库侧入口时,回到:
- [Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
- [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
4. 需要统一入口时,回到:
- [Game 模块总览](../game/index.md)
- [入门指南](../getting-started/index.md)

View File

@ -5,35 +5,34 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
# API 参考
里不再维护一份脱离源码演化的“伪 API 列表”
页不是签名索引,而是“先看哪个模块入口、再回哪里读 XML 文档”的导航页
当前 `GFramework` 的 API 参考链路以四类证据协同为准
最常见的阅读顺序是
1. 模块 README说明包关系、最小接入路径和目录边界
2. `docs/zh-CN` 专题页:说明采用顺序、生命周期和使用建议
3. 代码中的 XML 文档:说明公开 / 内部类型和关键成员的契约
4. 教程页:说明这些 API 在真实接入路径中的组合方式
1. 先按模块找到对应栏目入口
2. 再进专题页确认安装、生命周期和推荐接线方式
3. 最后回到源码中的 XML 文档核对具体契约
## 阅读顺序
### 想确认“该装哪个包、先看哪类 API”
先读模块 README再读对应栏目入口页:
先读站内入口页:
- 入门入口:[入门指南](../getting-started/index.md)
- 根模块地图:[仓库总览](https://github.com/GeWuYou/GFramework/blob/main/README.md)
- 安装与选包:[安装配置](../getting-started/installation.md)
### 想确认“这个功能属于哪个模块”
按下面的模块映射进入对应入口:
| 模块族 | 模块 README | 站内入口 | XML 文档关注点 |
| 模块族 | 先看什么 | 继续深入 | XML 文档关注点 |
| --- | --- | --- | --- |
| `Core` / `Core.Abstractions` | [Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)、[Core 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md) | [Core 栏目](../core/index.md)、[Core 抽象层说明](../abstractions/core-abstractions.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)、[CQRS 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Abstractions/README.md)、[CQRS 源码生成器说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md) | [CQRS 栏目](../core/cqrs.md)、[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md) | request / notification / handler / pipeline / registry / fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)、[Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)、[Game 源码生成器说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.SourceGenerators/README.md) | [Game 模块总览](../game/index.md)、[Game 抽象层说明](../abstractions/game-abstractions.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
| `Godot` / `Godot.SourceGenerators` | [Godot 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)、[Godot 源码生成器说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md) | [Godot 模块总览](../godot/index.md)、[Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [Ecs.Arch 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)、[ECS 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md) | [ECS 模块总览](../ecs/index.md)、[Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
| `Core` / `Core.Abstractions` | [Core 模块](../core/index.md) | [Core 抽象层说明](../abstractions/core-abstractions.md)、[快速开始](../getting-started/quick-start.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / registry / fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 模块总览](../game/index.md) | [Game 抽象层说明](../abstractions/game-abstractions.md)、[配置系统](../game/config-system.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
| `Godot` / `Godot.SourceGenerators` | [Godot 模块总览](../godot/index.md) | [Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [ECS 模块总览](../ecs/index.md) | [Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
## 先看 XML还是先看教程
@ -61,31 +60,23 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
- 最佳实践:[最佳实践](../best-practices/index.md)
- 故障排查:[故障排查](../troubleshooting.md)
## 当前边界
## 共享支撑层怎么看
- `GFramework.Core.SourceGenerators.Abstractions`
- `GFramework.Godot.SourceGenerators.Abstractions`
- `GFramework.SourceGenerators.Common`
这些目录当前不是独立消费模块,因此不单独维护站内 API 参考入口。它们的公开说明跟随所属模块 README 和
`source-generators` 栏目维护。
更准确地说:
这些目录当前不作为独立采用入口;阅读它们时,优先回到所属模块页和 `source-generators` 栏目,再根据需要下钻到具体源码目录。
- `*.SourceGenerators.Abstractions`
- 主要定义公开 attribute 和最小契约,供对应生成器与消费端项目共享。
- `GFramework.SourceGenerators.Common`
- 主要提供共享生成器基类、通用 diagnostics以及生成方法冲突等跨模块约束。
如果你在 `Core``CQRS``Game``Godot` 的生成器页面里遇到相似的 diagnostics 或冲突语义,优先把它理解为共享
支撑层在复用同一套规则,而不是把这些目录当成新的安装入口。
## 使用方式
把本页当成“API 阅读导航”而不是“签名快照”:
- 先选模块
- 再进 README 和专题页确认采用路径
- 再进专题页确认采用路径
- 最后回到代码里的 XML 文档核对具体契约
当 README、专题页和 XML 文档出现冲突时,以源码和测试所反映的当前实现为准。

View File

@ -61,7 +61,7 @@ await architecture.WaitUntilReadyAsync();
## 初始化时机
当前版本不再使用旧文档里的 `Init()` 入口。注册逻辑必须写在:
当前 `Architecture`注册逻辑必须写在:
```csharp
protected override void OnInitialize()
@ -77,7 +77,7 @@ protected override void OnInitialize()
4. 按阶段初始化 `Utility -> Model -> System`
5. 进入 `Ready`
如果你还看到旧示例里写 `protected override void Init()`,那就是过时内容
如果项目里仍保留 `protected override void Init()` 风格的旧代码,应迁移到 `OnInitialize()`
## 组件注册顺序

View File

@ -214,4 +214,4 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
- 上下文入口:[Context 上下文](./context.md)
- 生成器专题:[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)
- 协程接法:[协程系统](./coroutine.md)
- 模块说明:[CQRS 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)
- 选包与入口:[入门指南](../getting-started/index.md)

View File

@ -8,7 +8,7 @@ description: GFramework.Core 与 GFramework.Core.Abstractions 的运行时入口
`Core` 栏目对应 `GFramework` 的基础运行时层,主要覆盖 `GFramework.Core``GFramework.Core.Abstractions`,以及与之直接相邻的旧版
`Command` / `Query` 执行器和新版 `CQRS` 迁移入口。
如果你第一次接入框架,建议先把这里当作“运行时底座说明”,再按需进入 `Game``Godot` 或 Source Generators 栏目
如果你第一次接入框架,可以先把这里当作“运行时底座说明”:先确认 `Core` 解决什么问题、最小安装组合是什么,再决定什么时候转向 `CQRS``Game``Godot` 或源码生成器
## 模块与包关系
@ -32,7 +32,7 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
## 栏目覆盖范围
`Core` 栏目不是旧版“完整框架教程”的镜像,而是当前实现的入口导航。这里的页面按能力域组织:
这里的页面按能力域组织,适合按“我要接什么能力”而不是“我要读完所有目录”的方式进入
- 架构与生命周期
- [架构](./architecture.md)
@ -71,7 +71,7 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
如果你已经知道模块归属,但想确认公开类型的契约边界,建议按下面顺序阅读:
1. 先看[Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md),确认包关系和目录边界
1. 先读本页与 [Core 抽象层说明](../abstractions/core-abstractions.md),确认运行时和契约层边界
2. 再看本栏目对应专题页,确认采用顺序、生命周期与推荐接线方式
3. 最后回到源码中的 XML 文档,重点核对这些类型族:
- `Architecture` / `IArchitectureContext`
@ -148,7 +148,8 @@ public sealed class CounterArchitecture : Architecture
## 对应模块入口
- [Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
- [Core 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [入门指南](../getting-started/index.md)
- [Core 抽象层说明](../abstractions/core-abstractions.md)
- [CQRS 运行时](./cqrs.md)
- [源码生成器总览](../source-generators/index.md)
- [API 参考入口](../api-reference/index.md)
- [仓库总览](https://github.com/GeWuYou/GFramework/blob/main/README.md)

View File

@ -140,5 +140,5 @@ ecsModule.Update(deltaTime);
- ECS 模块总览:[ECS 模块总览](./index.md)
- 抽象契约页:[ECS 抽象层说明](../abstractions/ecs-arch-abstractions.md)
- 仓库模块说明:[Ecs.Arch 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)
- 选包与接入顺序:[入门指南](../getting-started/index.md)
- 统一 API / XML 导航:[API 参考](../api-reference/index.md)

View File

@ -1021,11 +1021,11 @@ var hotReload = loader.EnableHotReload(
## 工具形态建议
当前优先采用 `VS Code Extension` 形态即可覆盖仓库中已落地的主要工作流,包括 schema 校验、轻量表单、批量编辑和 raw YAML 回退。
对当前仓库已经落地的工作流而言,`VS Code Extension` 形态已经可以覆盖 schema 校验、轻量表单、批量编辑和 raw YAML 回退这条采用路径
如果你的团队出现以下需求,再评估独立 `Config Studio` 会更合适:
- 主要使用者变成非开发人员,且 VS Code 的安装与使用成本持续阻碍日常维护
- 配置维护主要由非开发角色承担,希望进一步降低 VS Code 的安装和使用门槛
- 需要更重的表格视图、跨表可视化关系编辑、复杂审批流或离线发布流程
- 插件形态已经明显受限于 VS Code Webview / Extension API而不是 schema 与工作流本身
- 已经沉淀出稳定的 schema 元数据约定,足以支撑单独工具的长期维护

View File

@ -25,7 +25,7 @@ description: GFramework.Game family 的运行时入口、采用顺序与源码
## 栏目覆盖范围
`Game` 栏目聚焦“游戏项目该怎么接这些运行时能力”,不再保留与当前实现脱节的泛化模块示例
`Game` 栏目聚焦“游戏项目该怎么把这些运行时能力接起来”适合先按目标判断你要接入的是配置、设置与存档、Scene / UI 路由,还是文件存储与序列化
当前栏目主要入口:
@ -115,20 +115,16 @@ IStorage storage = new FileStorage("GameData", serializer);
| `GFramework.Game.Abstractions` | `IConfigRegistry``ISaveRepository<TSaveData>``ISettingsSystem``ISceneRouter``IUiRouter` | 契约层边界,以及项目中哪些程序集只应依赖接口 |
| `GFramework.Game.SourceGenerators` | `SchemaConfigGenerator``ConfigSchemaDiagnostics` | schema 生成入口与诊断模型,确认配置系统的编译期链路 |
## 与真实接法的关系
## 运行时与生成器如何配合
本栏目内容以源码、`*.csproj`、模块说明页与 `ai-libs/` 下已验证的参考接法为准。
例如当前文档应优先和以下已验证事实保持一致:
- 配置系统采用 `YAML + JSON Schema + Source Generator`
- 设置持久化通常通过 `UnifiedSettingsDataRepository`
- 场景与 UI 路由依赖项目自己的 factory / root而不是框架替你绑定引擎对象
如果某个旧页面与这些事实冲突,应以源码和模块说明页为准,并在同一轮里修正文档。
- 运行时入口主要来自 `GFramework.Game`
- 只依赖接口或拆分业务层时,补充 `GFramework.Game.Abstractions`
- 需要静态内容配置类型和表包装生成时,再追加 `GFramework.Game.SourceGenerators`
- 需要编辑器侧内容维护工作流时,再看 [VS Code 配置工具](./config-tool.md)
## 对应模块入口
- [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
- [Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
- [仓库总览](https://github.com/GeWuYou/GFramework/blob/main/README.md)
- [入门指南](../getting-started/index.md)
- [Game 抽象层说明](../abstractions/game-abstractions.md)
- [源码生成器总览](../source-generators/index.md)
- [API 参考入口](../api-reference/index.md)

View File

@ -258,5 +258,5 @@ await sceneRouter.PopAsync();
1. [Game 模块总览](./index.md)
2. [UI 系统](./ui.md)
3. [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
4. [Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
3. [Game 抽象层说明](../abstractions/game-abstractions.md)
4. [API 参考](../api-reference/index.md)

View File

@ -46,7 +46,7 @@ IRuntimeTypeSerializer runtimeSerializer = new JsonSerializer();
## 配置生命周期
部分是当前实现最容易被旧文档说错的地方
里最需要先确认的是序列化器的配置生命周期
`JsonSerializer` 不会复制你传入的 `JsonSerializerSettings`。它会直接持有并复用这份实例,以及里面的 `Converters` 集合。

View File

@ -16,7 +16,7 @@ description: 以当前 SettingsModel、SettingsSystem 与相关测试为准,
- `SettingsModel<TRepository>`
- `SettingsSystem`
而不是旧文档里那种“只靠若干 `Get<T>() / Register(...)` 辅助方法就能自动完成一切的模型。
而不是只靠若干 `Get<T>() / Register(...)` 辅助方法就能自动完成一切的模型。
## 当前公开入口
@ -91,7 +91,7 @@ applicator 的职责不是保存数据,而是把设置结果作用到实际运
## 初始化与迁移的真实语义
`SettingsModel<TRepository>.InitializeAsync()` 的当前行为,比旧文档里“加载一下就好”更严格一些
`SettingsModel<TRepository>.InitializeAsync()` 会按更完整的初始化与迁移顺序工作
- 它会先调用 `ISettingsDataRepository.LoadAllAsync()`
- 再逐个匹配当前模型里已经登记的设置类型

View File

@ -329,5 +329,5 @@ uiRouter.Hide(modalHandle, UiLayer.Modal);
1. [Game 模块总览](./index.md)
2. [场景系统](./scene.md)
3. [AutoUiPage 生成器](../source-generators/auto-ui-page-generator.md)
4. [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
5. [Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
4. [Game 抽象层说明](../abstractions/game-abstractions.md)
5. [API 参考](../api-reference/index.md)

View File

@ -5,17 +5,19 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
# 入门指南
这一部分只回答三个问题:
如果你第一次接触 `GFramework`,或者还在判断“应该先装哪些包、先看哪一组文档”,先从这里开始最省时间。
1. `GFramework` 由哪些模块组成
2. 第一次接入应该从哪个包开始
3. 最小可运行路径是什么
这组页面的目标只有一个:帮你用最短路径找到适合当前项目的运行时入口、安装组合和下一步阅读顺序。
如果你还没决定具体用法,先阅读本栏目;如果你已经明确要用某个模块,直接进入对应模块的说明页会更快。
## 适合谁先读本栏
## 推荐起步路径
- 第一次接入 `GFramework`,还没决定该从 `Core``Game``Godot` 还是 `CQRS` 开始
- 想先确认最小安装组合,再决定是否追加源码生成器
- 想先跑通一个可运行骨架,再深入某个专题页
### 只想先把架构跑起来
## 按目标选择起步路线
### 基础运行时起步
`Core` 开始:
@ -33,8 +35,9 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
- [Core 模块总览](../core/index.md)
- [快速开始](./quick-start.md)
- [安装配置](./installation.md)
### 想用新版 CQRS
### 新版 CQRS 请求流
`Core` 基础上补:
@ -51,9 +54,9 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
对应文档:
- [CQRS 运行时](../core/cqrs.md)
- 模块说明:[CQRS 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)
- [安装配置](./installation.md)
### 想做游戏运行时
### 游戏运行时与内容配置
`Core` 基础上按需补:
@ -70,9 +73,10 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
对应文档:
- [Game 模块总览](../game/index.md)
- 模块说明:[Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
- [配置系统](../game/config-system.md)
- [安装配置](./installation.md)
### 想接入 Godot
### Godot 项目接入
继续叠加:
@ -81,9 +85,10 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
对应文档:
- [Godot 模块总览](../godot/index.md)
- 模块说明:[Godot 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)
- [Godot 集成教程](../tutorials/godot-integration.md)
- [安装配置](./installation.md)
## Source Generators 什么时候装
## 什么时候追加源码生成器
只在需要编译期生成代码时再装:
@ -99,14 +104,15 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
- 为 CQRS handlers 生成注册表
- 生成 Godot 节点、场景和 UI 包装代码
继续阅读:
- [源码生成器总览](../source-generators/index.md)
- [配置系统](../game/config-system.md)
- [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)
## 建议阅读顺序
1. [快速开始](./quick-start.md)
2. 你准备使用的模块说明页
3. 对应栏目页,例如 `core``game``godot`
4. 需要更完整示例时,再进入 `tutorials/`
## 注意
- 旧文档里有一些早期示例已经和当前 API 漂移。本栏目以后只保留经过代码或测试核对的最小路径。
- 若根 README、模块 README 与某篇专题页冲突,以模块 README 和当前代码为准。
2. [安装配置](./installation.md)
3. 按你的目标进入 [基础运行时](../core/index.md)、[游戏运行时](../game/index.md)、[Godot 集成](../godot/index.md) 或 [源码生成器](../source-generators/index.md)
4. 需要完整示例时,再进入 [教程总览](../tutorials/index.md)

View File

@ -5,24 +5,28 @@ description: 说明 GFramework 各运行时与 source generator 包的安装选
# 安装配置
GFramework 提供多种安装方式,您可以根据项目需求选择合适的包进行安装
GFramework 采用按模块拆分的安装路径。先确认你要接入的是哪一层运行时、抽象层或源码生成器,再决定安装组合,会比直接把所有包一次性带进来更稳妥
## 包选择说明
GFramework 采用模块化设计,不同包提供不同的功能:
| 包名 | 说明 | 适用场景 |
|---------------------------------------------|--------------|--------------------------------|
| `GeWuYou.GFramework` | 聚合元包 | 快速试用、原型开发 |
| `GeWuYou.GFramework.Core` | 核心框架 | 生产项目推荐 |
| `GeWuYou.GFramework.Cqrs` | CQRS runtime | 命令/查询/通知分发与处理器注册 |
| `GeWuYou.GFramework.Cqrs.Abstractions` | CQRS 抽象契约 | CQRS 契约、handler 接口与共享抽象 |
| `GeWuYou.GFramework.Game` | 游戏模块 | 需要游戏特定功能 |
| `GeWuYou.GFramework.Godot` | Godot集成 | Godot项目必需 |
| `GeWuYou.GFramework.Core.SourceGenerators` | Core 源码生成器 | `[Log]``[ContextAware]`、架构注入等 |
| `GeWuYou.GFramework.Game.SourceGenerators` | Game 源码生成器 | 配置 schema / 配表生成 |
| `GeWuYou.GFramework.Godot.SourceGenerators` | Godot 源码生成器 | Godot 节点、UI、项目元数据生成 |
| `GeWuYou.GFramework.Cqrs.SourceGenerators` | CQRS 源码生成器 | 处理器注册表生成 |
| 包名 | 说明 | 适用场景 |
| --- | --- | --- |
| `GeWuYou.GFramework` | 聚合元包 | 快速试用、原型开发,或先起一个最小运行时骨架 |
| `GeWuYou.GFramework.Core` | Core 运行时 | 生产项目推荐的最小运行时起点 |
| `GeWuYou.GFramework.Core.Abstractions` | Core 抽象契约 | 面向接口开发、测试替身、插件化拆分 |
| `GeWuYou.GFramework.Cqrs` | CQRS runtime | 命令 / 查询 / 通知分发与处理器注册 |
| `GeWuYou.GFramework.Cqrs.Abstractions` | CQRS 抽象契约 | 共享 request、handler 与 pipeline 契约 |
| `GeWuYou.GFramework.Game` | Game 运行时 | 配置、存储、设置、Scene、UI 等游戏层能力 |
| `GeWuYou.GFramework.Game.Abstractions` | Game 抽象契约 | 共享 `IConfigRegistry``ISceneRouter``IUiRouter` 等接口 |
| `GeWuYou.GFramework.Godot` | Godot 集成 | Godot 项目的运行时接线、节点扩展与宿主适配 |
| `GeWuYou.GFramework.Ecs.Arch` | Arch ECS 运行时 | 需要 `UseArch(...)`、默认 `World` 注册与系统桥接 |
| `GeWuYou.GFramework.Ecs.Arch.Abstractions` | Arch ECS 抽象契约 | 只共享 ECS 模块接口、配置对象与宿主循环边界 |
| `GeWuYou.GFramework.Core.SourceGenerators` | Core 源码生成器 | `[Log]``[ContextAware]`、架构注入等 |
| `GeWuYou.GFramework.Game.SourceGenerators` | Game 源码生成器 | 配置 schema、配置类型、表包装与注册辅助生成 |
| `GeWuYou.GFramework.Godot.SourceGenerators` | Godot 源码生成器 | Godot 节点、UI、项目元数据生成 |
| `GeWuYou.GFramework.Cqrs.SourceGenerators` | CQRS 源码生成器 | 处理器注册表生成 |
当前 NuGet 发布按模块拆分 source generator 包,不存在 `GeWuYou.GFramework.SourceGenerators` 聚合包。
@ -33,6 +37,16 @@ GFramework 采用模块化设计,不同包提供不同的功能:
它不会自动带上 `Cqrs``Godot` 或任何 `*.SourceGenerators` 包。如果你需要这些能力,请按模块单独安装。
## 推荐组合
- 最小运行时:`GeWuYou.GFramework.Core` + `GeWuYou.GFramework.Core.Abstractions`
- 新版 CQRS在 Core 基础上追加 `GeWuYou.GFramework.Cqrs` + `GeWuYou.GFramework.Cqrs.Abstractions`
- Game 配置工作流:在 Core 基础上追加 `GeWuYou.GFramework.Game` + `GeWuYou.GFramework.Game.Abstractions` + `GeWuYou.GFramework.Game.SourceGenerators`
- Godot 项目:在所需运行时基础上追加 `GeWuYou.GFramework.Godot`,需要生成器辅助时再加 `GeWuYou.GFramework.Godot.SourceGenerators`
- Arch ECS直接安装 `GeWuYou.GFramework.Ecs.Arch`;如果只想共享宿主循环或接口边界,可改为 `GeWuYou.GFramework.Ecs.Arch.Abstractions`
如果你准备采用 AI-First 配置工作流,可以继续阅读 [游戏内容配置系统](../game/config-system.md) 与 [VS Code 配置工具](../game/config-tool.md)。
## 安装方式
### 1. 使用 .NET CLI推荐
@ -50,6 +64,10 @@ dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
dotnet add package GeWuYou.GFramework.Game
dotnet add package GeWuYou.GFramework.Game.Abstractions
# Arch ECS
dotnet add package GeWuYou.GFramework.Ecs.Arch
dotnet add package GeWuYou.GFramework.Ecs.Arch.Abstractions
# Godot 集成(仅 Godot 项目需要)
dotnet add package GeWuYou.GFramework.Godot
@ -88,6 +106,10 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
<!-- 游戏模块 -->
<PackageReference Include="GeWuYou.GFramework.Game" Version="1.0.0" />
<PackageReference Include="GeWuYou.GFramework.Game.Abstractions" Version="1.0.0" />
<!-- Arch ECS -->
<PackageReference Include="GeWuYou.GFramework.Ecs.Arch" Version="1.0.0" />
<PackageReference Include="GeWuYou.GFramework.Ecs.Arch.Abstractions" Version="1.0.0" />
<!-- Godot 集成 -->
<PackageReference Include="GeWuYou.GFramework.Godot" Version="1.0.0" />
@ -143,10 +165,9 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
如果你想排除局部导入,可以继续在项目文件中添加排除项:
```xml
<ItemGroup>
<GFrameworkExcludedUsing Include="GFramework.Core.Environment"/>
<GFrameworkExcludedUsing Include="GFramework.Godot.Extensions"/>
<GFrameworkExcludedUsing Include="GFramework.Core.Environment" />
<GFrameworkExcludedUsing Include="GFramework.Godot.Extensions" />
</ItemGroup>
```
@ -223,7 +244,7 @@ dotnet build
确保:
- Godot 版本 >= 4.5
- 项目环境与当前文档保持在 Godot 4.6.2 基线
- 已正确安装 Godot C# 模板
- 项目引用了正确的 Godot 包

Some files were not shown because too many files have changed in this diff Show More