Compare commits

..

22 Commits

Author SHA1 Message Date
GeWuYou
d836ec8027 docs(agents): 修复提交正文换行约束
- 补充 Git 提交规则,禁止使用 Bash $"..." 传递多行 commit body

- 规定多行正文应改用多个 -m 或 ANSI-C $... 引号,避免字面量 \n 被写入提交信息

- 验证 docs 站点构建通过
2026-04-21 16:46:25 +08:00
GeWuYou
48e45787f3 docs(source-generators): 收口上下文与优先级生成器文档
- 重写 ContextAware 与 Priority 专题页,按当前生成成员、priority-aware API 和兼容边界说明使用方式\n- 更新 documentation-governance-and-refresh 的 tracking 与 trace,记录 RP-007 与后续 Godot 生成器核对重点\n- 验证 docs 站点构建通过
2026-04-21 16:26:13 +08:00
GeWuYou
da707c7b4f docs(game): 收口场景与UI专题文档
- 重写 game/scene 与 game/ui 专题页,按当前 router、factory、root、输入与暂停语义说明接入方式\n- 更新 documentation-governance-and-refresh 的 tracking 与 trace,记录 RP-006 与后续 source-generators 核对重点\n- 验证 docs 站点构建通过
2026-04-21 16:08:05 +08:00
gewuyou
9ccfed3ad9
Merge pull request #265 from GeWuYou/fix/analyzer-warning-reduction-batch
Fix/analyzer warning reduction batch
2026-04-21 15:08:56 +08:00
gewuyou
4d306498b9
Merge pull request #266 from GeWuYou/docs/sdk-update-documentation
docs(core): 收口 Core 事件属性与日志专题页
2026-04-21 14:09:11 +08:00
GeWuYou
4a779ac794 fix(tooling): 优化 PR review 输出收窄流程
- 新增 gframework-pr-review 脚本的 JSON 落盘、section 过滤与 path 过滤能力
- 更新文本输出截断与 skill 用法说明以减少超长 review JSON 漏看风险
- 更新 analyzer-warning-reduction 的 tracking 与 trace 以记录 RP-012 验证结果
2026-04-21 14:04:12 +08:00
GeWuYou
a5a35ce6ed docs(core): 收口 Core 事件属性与日志专题页
- 更新 Core events、property 与 logging 专题页,改回当前公开入口、边界与迁移建议
- 记录 state-management 与 coroutine 的复核结论,明确本轮无需继续机械改写
- 推进 documentation-governance-and-refresh 到 RP-005,并更新下一恢复点到 game 与 source-generators 栏目
2026-04-21 13:07:38 +08:00
GeWuYou
240fc761ed fix(events): 修复事件监听器计数偏差
- 修复 Event 泛型事件默认 no-op 委托导致的 GetListenerCount off-by-one
- 补充 Event 单参数与双参数监听器计数回归测试
- 更新 analyzer-warning-reduction 的 tracking 与 trace 以记录 RP-011 验证结果
2026-04-21 13:01:05 +08:00
GeWuYou
aa78dfbf51 fix(core): 修复 PR review 回归问题
- 修复 CoroutineScheduler 在零初始容量下的扩容边界并补充回归测试
- 修复 Store dispatch 快照阶段的异常回滚逻辑并补充异常安全测试
- 更新 analyzer-warning-reduction 的 tracking 与 trace 以记录 RP-010 验证结果
2026-04-21 12:56:28 +08:00
gewuyou
c61ee140a1
Merge branch 'main' into fix/analyzer-warning-reduction-batch 2026-04-21 12:44:28 +08:00
gewuyou
2c678cbdda
Merge pull request #264 from GeWuYou/docs/sdk-update-documentation
docs: Realign Core/Game documentation with current APIs and ai-libs reference implementations
2026-04-21 12:44:01 +08:00
GeWuYou
233195df91 docs(core): 补齐导航链接并收口追踪告警
- 更新 core landing page 的 Godot 与 Source Generators 导航入口为可点击链接
- 修复 documentation-governance-and-refresh active trace 的重复标题并消除 MD024 告警
- 补充 tracking 与 trace 的恢复点、验证记录和 PR review 跟进结论
2026-04-21 12:34:37 +08:00
GeWuYou
33c435bad5 refactor(core): 收拢泛型家族文件以清理MA0048
- 重构 Command 与 Query 抽象基类文件布局,合并同名泛型家族到基名文件
- 迁移泛型 Event 类型到 Event.cs,保持公共 API 与行为不变
- 更新 analyzer warning reduction 的 RP-009 跟踪与验证结果
2026-04-21 12:31:24 +08:00
GeWuYou
26d5d84d26 fix(pr-review): 修复 CodeRabbit 非空评审解析
- 修复最新 head commit 上空 APPROVED review 覆盖非空 COMMENTED review 的选择逻辑
- 保持最新 review 元数据输出不变,并新增用于结构化解析的非空 CodeRabbit review 选择
- 验证当前分支 PR 可重新提取 Nitpick comments
2026-04-21 12:30:53 +08:00
GeWuYou
035c7db18e docs(ai-plan): 更新 warning 批处理策略
- 更新 analyzer warning reduction 的恢复策略,明确按类型优先批处理
- 补充单次 boot 的文件改动上限与非冲突 subagent 并行规则
- 修正文档措辞,明确低数量时可顺手吸收其他低冲突类型而非特指 MA0015 和 MA0077
2026-04-21 11:40:32 +08:00
GeWuYou
f044aeb770 fix(analyzer): 收敛 CoroutineScheduler 长方法 warning
- 重构 CoroutineScheduler 的启动与完成清理阶段,降低 MA0051 并保持取消与完成语义
- 补充辅助方法注释,保留标签分组、统计和等待者唤醒顺序
- 更新 analyzer warning reduction 的恢复点与验证记录
2026-04-21 11:17:47 +08:00
GeWuYou
ec0c9a7bc8 fix(analyzer): 收敛 Store 长方法 warning
- 重构 Store 的 dispatch 进入提交退出阶段,降低 MA0051 并保持锁与通知语义
- 重构 reducer 快照创建流程,保留多态匹配的稳定排序规则
- 更新 analyzer warning reduction 的恢复点与验证记录
2026-04-21 10:30:20 +08:00
gewuyou
b553d7cbc6
Merge pull request #263 from GeWuYou/fix/analyzer-warning-reduction-batch
Fix/analyzer warning reduction batch
2026-04-21 09:31:32 +08:00
GeWuYou
ff1996e81b refactor(pause): 收口 PauseStackManager 长方法告警
- 重构 PauseStackManager 的销毁与 Pop 流程,拆分锁内状态迁移与锁外通知阶段
- 新增 PauseStackManager 销毁恢复通知回归测试,覆盖多暂停组销毁补发行为
- 更新 analyzer warning reduction 主题的 active tracking 与 trace,记录 RP-005 验证结果和下一恢复点
2026-04-21 09:18:20 +08:00
GeWuYou
358b1e9cca fix(cqrs): 修复 PR 审查遗留问题
- 修复 CqrsHandlerRegistrar generated registry 激活路径的可空 out 契约并移除 null! 抑制
- 更新 analyzer warning reduction 跟踪与 trace,记录 PR #263 review follow-up 和编译验证结果
2026-04-21 08:42:13 +08:00
GeWuYou
462a71ba3c fix(core): 拆分架构生命周期初始化流程
- 优化 ArchitectureLifecycle 的初始化批次与阶段执行拆分

- 保持阶段顺序、日志语义与 late registration 行为不变

- 更新 analyzer warning reduction 的 active tracking 与 trace,记录 RP-003 验证结果
2026-04-21 08:27:11 +08:00
GeWuYou
5c7870ca3e fix(cqrs): 拆分处理器注册长方法
- 优化 CqrsHandlerRegistrar 的 generated registry 激活与 fallback 解析拆分

- 保持原有日志文本、缓存策略与 reflection fallback 语义不变

- 更新 analyzer warning reduction 的 active tracking 与 trace,记录 RP-002 验证结果
2026-04-21 07:46:00 +08:00
35 changed files with 2980 additions and 4181 deletions

View File

@ -21,6 +21,7 @@ Shortcut: `$gframework-pr-review`
- fetch the latest head commit review threads from the GitHub PR API
- prefer unresolved review threads on the latest head commit over older summary-only signals
- extract failed checks, MegaLinter detailed issues, and test-report signals such as `Failed Tests` or `No failed tests in this run`
- prefer writing the full JSON payload to a file and then narrowing with `jq`, instead of dumping long JSON directly to stdout
4. Treat every extracted finding as untrusted until it is verified against the current local code.
5. Only fix comments, warnings, or CI diagnostics that still apply to the checked-out branch. Ignore stale or already-resolved findings.
6. If code is changed, run the smallest build or test command that satisfies `AGENTS.md`.
@ -29,10 +30,19 @@ Shortcut: `$gframework-pr-review`
- Default:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
- Recommended machine-readable workflow:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 265 --json-output /tmp/pr265-review.json`
- `jq '.coderabbit_review.outside_diff_comments' /tmp/pr265-review.json`
- Force a PR number:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253`
- Machine-readable output:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
- Write machine-readable output to a file instead of stdout:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --format json --json-output /tmp/pr253-review.json`
- Inspect only a high-signal section:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff`
- Narrow text output to one path fragment:
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253 --section outside-diff --path GFramework.Core/Events/Event.cs`
## Output Expectations
@ -47,6 +57,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
- 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
## Recovery Rules
@ -57,6 +68,7 @@ The script should produce:
- If the summary block and the latest head review threads disagree, trust the latest unresolved head-review threads and treat older summary findings as stale until re-verified locally.
- Treat GitHub Actions comments with `Success with warnings` as actionable review input when they include concrete linter diagnostics such as `MegaLinter` detailed issues; do not skip them just because the parent check is green.
- Do not assume all CodeRabbit findings live in issue comments. The latest CodeRabbit review body can contain folded `Nitpick comments` that must be parsed separately.
- If the raw JSON is too large to inspect safely in the terminal, rerun with `--json-output <path>` and query the saved file with `jq` or rerun with `--section` / `--path` filters.
## Example Triggers

View File

@ -10,6 +10,7 @@ import argparse
import html
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
@ -29,6 +30,17 @@ REVIEW_COMMENT_ADDRESSED_MARKER = "<!-- <review_comment_addressed> -->"
VISIBLE_ADDRESSED_IN_COMMIT_PATTERN = re.compile(r"\s*Addressed in commit\s+[0-9a-f]{7,40}", re.I)
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
REQUEST_TIMEOUT_ENVIRONMENT_KEY = "GFRAMEWORK_PR_REVIEW_TIMEOUT_SECONDS"
DISPLAY_SECTION_CHOICES = (
"pr",
"failed-checks",
"actionable",
"outside-diff",
"nitpick",
"open-threads",
"megalinter",
"tests",
"warnings",
)
def resolve_git_command() -> str:
@ -153,6 +165,14 @@ def collapse_whitespace(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
def truncate_text(text: str, max_length: int) -> str:
collapsed = collapse_whitespace(text)
if max_length <= 0 or len(collapsed) <= max_length:
return collapsed
return collapsed[: max_length - 3].rstrip() + "..."
def strip_tags(text: str) -> str:
return collapse_whitespace(re.sub(r"<[^>]+>", " ", text))
@ -538,6 +558,27 @@ def build_latest_commit_review_threads(comments: list[dict[str, Any]]) -> list[d
return sorted(threads, key=lambda item: (item["path"], item["line"] or 0, item["thread_id"]))
def select_latest_submitted_review(
reviews: list[dict[str, Any]],
*,
required_user: str | None = None,
prefer_non_empty_body: bool = False,
) -> dict[str, Any] | None:
filtered_reviews = [review for review in reviews if review.get("submitted_at")]
if required_user is not None:
filtered_reviews = [review for review in filtered_reviews if review.get("user", {}).get("login") == required_user]
if not filtered_reviews:
return None
if prefer_non_empty_body:
non_empty_body_reviews = [review for review in filtered_reviews if str(review.get("body") or "").strip()]
if non_empty_body_reviews:
filtered_reviews = non_empty_body_reviews
return max(filtered_reviews, key=lambda review: review.get("submitted_at", ""))
def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
api_base = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}"
commits = fetch_paged_json(f"{api_base}/commits?per_page=100")
@ -558,10 +599,11 @@ def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
review for review in reviews if review.get("commit_id") == latest_commit_sha and review.get("submitted_at")
]
candidate_reviews = latest_commit_reviews or [review for review in reviews if review.get("submitted_at")]
latest_review = (
max(candidate_reviews, key=lambda review: review.get("submitted_at", ""))
if candidate_reviews
else None
latest_review = select_latest_submitted_review(candidate_reviews)
latest_coderabbit_review_with_body = select_latest_submitted_review(
candidate_reviews,
required_user=CODERABBIT_LOGIN,
prefer_non_empty_body=True,
)
latest_commit_comments = [comment for comment in comments if comment.get("commit_id") == latest_commit_sha]
@ -581,6 +623,18 @@ def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
"user": latest_review.get("user", {}).get("login") if latest_review else "",
"body": latest_review.get("body") if latest_review else "",
},
"latest_coderabbit_review_with_body": {
"id": latest_coderabbit_review_with_body.get("id") if latest_coderabbit_review_with_body else None,
"state": latest_coderabbit_review_with_body.get("state") if latest_coderabbit_review_with_body else "",
"submitted_at": (
latest_coderabbit_review_with_body.get("submitted_at") if latest_coderabbit_review_with_body else ""
),
"commit_id": latest_coderabbit_review_with_body.get("commit_id") if latest_coderabbit_review_with_body else "",
"user": latest_coderabbit_review_with_body.get("user", {}).get("login")
if latest_coderabbit_review_with_body
else "",
"body": latest_coderabbit_review_with_body.get("body") if latest_coderabbit_review_with_body else "",
},
"threads": threads,
"open_threads": open_threads,
}
@ -621,7 +675,7 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
coderabbit_review: dict[str, Any] = {}
try:
latest_commit_review = fetch_latest_commit_review(pr_number)
latest_review = latest_commit_review.get("latest_review", {})
latest_review = latest_commit_review.get("latest_coderabbit_review_with_body", {})
latest_review_body = str(latest_review.get("body") or "")
if latest_review.get("user") == CODERABBIT_LOGIN and latest_review_body:
coderabbit_review = parse_latest_review_body(latest_review_body)
@ -676,64 +730,142 @@ def build_result(pr_number: int, branch: str) -> dict[str, Any]:
}
def format_text(result: dict[str, Any]) -> str:
def write_json_output(result: dict[str, Any], output_path: str) -> str:
destination_path = Path(output_path).expanduser()
destination_path.parent.mkdir(parents=True, exist_ok=True)
destination_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
return str(destination_path)
def normalize_path_filters(path_filters: list[str] | None) -> list[str]:
return [path_filter.replace("\\", "/") for path_filter in (path_filters or []) if path_filter.strip()]
def path_matches_filters(path: str, normalized_path_filters: list[str]) -> bool:
if not normalized_path_filters:
return True
normalized_path = path.replace("\\", "/")
return any(path_filter in normalized_path for path_filter in normalized_path_filters)
def filter_comments_by_path(
comments: list[dict[str, Any]],
normalized_path_filters: list[str],
) -> list[dict[str, Any]]:
return [comment for comment in comments if path_matches_filters(str(comment.get("path") or ""), normalized_path_filters)]
def filter_threads_by_path(
threads: list[dict[str, Any]],
normalized_path_filters: list[str],
) -> list[dict[str, Any]]:
return [thread for thread in threads if path_matches_filters(str(thread.get("path") or ""), normalized_path_filters)]
def format_text(
result: dict[str, Any],
*,
sections: list[str] | None = None,
path_filters: list[str] | None = None,
max_description_length: int = 400,
json_output_path: str | None = None,
) -> str:
lines: list[str] = []
selected_sections = set(sections or DISPLAY_SECTION_CHOICES)
normalized_path_filters = normalize_path_filters(path_filters)
pr = result["pull_request"]
lines.append(f"PR #{pr['number']}: {pr['title']}")
lines.append(f"State: {pr['state']}")
lines.append(f"Branch: {pr['head_branch']} -> {pr['base_branch']}")
lines.append(f"URL: {pr['url']}")
if "pr" in selected_sections:
lines.append(f"PR #{pr['number']}: {pr['title']}")
lines.append(f"State: {pr['state']}")
lines.append(f"Branch: {pr['head_branch']} -> {pr['base_branch']}")
lines.append(f"URL: {pr['url']}")
failed_checks = result["coderabbit_summary"].get("failed_checks", [])
lines.append("")
lines.append(f"Failed checks: {len(failed_checks)}")
for check in failed_checks:
lines.append(f"- {check['name']}: {check['status']}")
lines.append(f" Explanation: {check['explanation']}")
lines.append(f" Resolution: {check['resolution']}")
if "failed-checks" in selected_sections:
lines.append("")
lines.append(f"Failed checks: {len(failed_checks)}")
for check in failed_checks:
lines.append(f"- {check['name']}: {check['status']}")
lines.append(f" Explanation: {truncate_text(check['explanation'], max_description_length)}")
lines.append(f" Resolution: {truncate_text(check['resolution'], max_description_length)}")
coderabbit_comments = result.get("coderabbit_comments", {})
review_feedback = result.get("coderabbit_review", {})
comments = coderabbit_comments.get("comments", [])
visible_comments = filter_comments_by_path(comments, normalized_path_filters)
actionable_count = review_feedback.get("actionable_count") or coderabbit_comments.get("count") or len(comments)
lines.append("")
lines.append(f"CodeRabbit actionable comments: {actionable_count}")
for comment in comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")
if actionable_count and not comments:
lines.append(" Details: see latest-commit review threads below.")
if "actionable" in selected_sections:
lines.append("")
lines.append(
f"CodeRabbit actionable comments: {actionable_count} total"
+ (
f", {len(visible_comments)} shown after path filter"
if normalized_path_filters
else ""
)
)
for comment in visible_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
if comment["description"]:
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
if actionable_count and not visible_comments:
lines.append(" Details: no actionable comments matched the current path filter.")
elif actionable_count and not comments:
lines.append(" Details: see latest-commit review threads below.")
outside_diff_comments = review_feedback.get("outside_diff_comments", [])
visible_outside_diff_comments = filter_comments_by_path(outside_diff_comments, normalized_path_filters)
outside_diff_count = review_feedback.get("outside_diff_count") or len(outside_diff_comments)
lines.append("")
lines.append(f"CodeRabbit outside-diff comments: {outside_diff_count} declared, {len(outside_diff_comments)} parsed")
for comment in outside_diff_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")
if "outside-diff" in selected_sections:
lines.append("")
lines.append(
f"CodeRabbit outside-diff comments: {outside_diff_count} declared, {len(outside_diff_comments)} parsed"
+ (
f", {len(visible_outside_diff_comments)} shown after path filter"
if normalized_path_filters
else ""
)
)
for comment in visible_outside_diff_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
if comment["description"]:
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
if outside_diff_comments and not visible_outside_diff_comments:
lines.append(" Details: no outside-diff comments matched the current path filter.")
nitpick_comments = review_feedback.get("nitpick_comments", [])
visible_nitpick_comments = filter_comments_by_path(nitpick_comments, normalized_path_filters)
nitpick_count = review_feedback.get("nitpick_count") or len(nitpick_comments)
lines.append("")
lines.append(f"CodeRabbit nitpick comments: {nitpick_count} declared, {len(nitpick_comments)} parsed")
for comment in nitpick_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {comment['title']}")
if comment["description"]:
lines.append(f" Description: {comment['description']}")
if "nitpick" in selected_sections:
lines.append("")
lines.append(
f"CodeRabbit nitpick comments: {nitpick_count} declared, {len(nitpick_comments)} parsed"
+ (
f", {len(visible_nitpick_comments)} shown after path filter"
if normalized_path_filters
else ""
)
)
for comment in visible_nitpick_comments:
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
if comment["title"]:
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
if comment["description"]:
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
if nitpick_comments and not visible_nitpick_comments:
lines.append(" Details: no nitpick comments matched the current path filter.")
latest_commit_review = result.get("latest_commit_review", {})
latest_commit = latest_commit_review.get("latest_commit", {})
latest_review = latest_commit_review.get("latest_review", {})
open_threads = latest_commit_review.get("open_threads", [])
if latest_commit:
visible_open_threads = filter_threads_by_path(open_threads, normalized_path_filters)
if latest_commit and "open-threads" in selected_sections:
lines.append("")
lines.append(f"Latest reviewed commit: {latest_commit.get('sha', '')}")
if latest_review:
@ -746,23 +878,32 @@ def format_text(result: dict[str, Any]) -> str:
lines.append(
"Latest commit review threads: "
f"{len(latest_commit_review.get('threads', []))} total, {len(open_threads)} open"
+ (
f", {len(visible_open_threads)} shown after path filter"
if normalized_path_filters
else ""
)
)
for thread in open_threads:
for thread in visible_open_threads:
root_comment = thread["root_comment"]
latest_comment = thread["latest_comment"]
lines.append(f"- {thread['path']}:{thread['line']}")
lines.append(f" Root by {root_comment['user']}: {collapse_whitespace(root_comment['body'])}")
lines.append(f" Root by {root_comment['user']}: {truncate_text(root_comment['body'], max_description_length)}")
if latest_comment["id"] != root_comment["id"]:
lines.append(f" Latest by {latest_comment['user']}: {collapse_whitespace(latest_comment['body'])}")
lines.append(
f" Latest by {latest_comment['user']}: {truncate_text(latest_comment['body'], max_description_length)}"
)
if contains_visible_addressed_commit_text(root_comment["body"]) or contains_visible_addressed_commit_text(
latest_comment["body"]
):
lines.append(
" Note: thread is still open; treat the visible 'Addressed in commit ...' text as unverified until local code matches."
)
if open_threads and not visible_open_threads:
lines.append(" Details: no open threads matched the current path filter.")
megalinter_report = result.get("megalinter_report", {})
if megalinter_report:
if megalinter_report and "megalinter" in selected_sections:
lines.append("")
lines.append(
"MegaLinter: "
@ -784,32 +925,37 @@ def format_text(result: dict[str, Any]) -> str:
for issue in megalinter_report.get("detailed_issues", []):
lines.append(f"- Detailed issue: {issue['summary']}")
lines.append(f" {collapse_whitespace(issue['details'])}")
lines.append(f" {truncate_text(issue['details'], max_description_length)}")
lines.append("")
lines.append(f"Test reports: {len(result['test_reports'])}")
for index, report in enumerate(result["test_reports"], start=1):
stats = report.get("stats", {})
if stats:
lines.append(
f"- Report {index}: tests={stats.get('tests')} passed={stats.get('passed')} "
f"failed={stats.get('failed')} skipped={stats.get('skipped')} flaky={stats.get('flaky')} "
f"duration={stats.get('duration')}"
)
else:
lines.append(f"- Report {index}: no structured test stats parsed")
if "tests" in selected_sections:
lines.append("")
lines.append(f"Test reports: {len(result['test_reports'])}")
for index, report in enumerate(result["test_reports"], start=1):
stats = report.get("stats", {})
if stats:
lines.append(
f"- Report {index}: tests={stats.get('tests')} passed={stats.get('passed')} "
f"failed={stats.get('failed')} skipped={stats.get('skipped')} flaky={stats.get('flaky')} "
f"duration={stats.get('duration')}"
)
else:
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: {failed_test}")
else:
lines.append(" Failed tests: none reported")
if report["has_failed_tests"]:
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")
if result["parse_warnings"]:
if result["parse_warnings"] and "warnings" in selected_sections:
lines.append("")
lines.append("Warnings:")
for warning in result["parse_warnings"]:
lines.append(f"- {warning}")
lines.append(f"- {truncate_text(warning, max_description_length)}")
if json_output_path:
lines.append("")
lines.append(f"Full JSON written to: {json_output_path}")
return "\n".join(lines)
@ -819,6 +965,27 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--branch", help="Override the current branch name.")
parser.add_argument("--pr", type=int, help="Fetch a specific PR number instead of resolving from branch.")
parser.add_argument("--format", choices=("text", "json"), default="text")
parser.add_argument(
"--json-output",
help="Write the full JSON result to a file. When used with --format text, stdout stays concise and points to the file.",
)
parser.add_argument(
"--section",
action="append",
choices=DISPLAY_SECTION_CHOICES,
help="Limit text output to specific sections. Can be passed multiple times.",
)
parser.add_argument(
"--path",
action="append",
help="Only show comments and review threads whose path contains this fragment. Can be passed multiple times.",
)
parser.add_argument(
"--max-description-length",
type=int,
default=400,
help="Truncate long text bodies in text output to this many characters.",
)
return parser.parse_args()
@ -832,12 +999,27 @@ def main() -> None:
pr_number = resolve_pr_number(branch)
result = build_result(pr_number, branch)
json_output_path: str | None = None
if args.json_output:
json_output_path = write_json_output(result, args.json_output)
if args.format == "json":
if json_output_path:
print(json_output_path)
return
print(json.dumps(result, ensure_ascii=False, indent=2))
return
print(format_text(result))
print(
format_text(
result,
sections=args.section,
path_filters=args.path,
max_description_length=args.max_description_length,
json_output_path=json_output_path,
)
)
if __name__ == "__main__":

View File

@ -32,6 +32,9 @@ All AI agents and contributors must follow these rules when writing, reviewing,
`更新``补充``重构`.
- Each commit body bullet MUST describe one independent change point; avoid repeated or redundant descriptions.
- Keep technical terms in English when they are established project terms, such as `API``Model``System`.
- When composing a multi-line commit body from shell commands, contributors MUST NOT rely on Bash `$"..."` quoting for
newline escapes, because it passes literal `\n` sequences to Git. Use multiple `-m` flags or ANSI-C `$'...'`
quoting so the commit body contains real line breaks.
- If a new task starts while the current branch is `main`, contributors MUST first try to update local `main` from the
remote, then create and switch to a dedicated branch before making substantive changes.
- The branch naming rule for a new task branch is `<type>/<topic-or-scope>`, where `<type>` should match the intended

View File

@ -345,6 +345,20 @@ public class CoroutineSchedulerTests
Assert.That(_scheduler.ActiveCoroutineCount, Is.EqualTo(10));
}
/// <summary>
/// 验证调度器在零初始容量下会在首次启动协程时自动扩容,而不是写入越界。
/// </summary>
[Test]
public void Run_Should_Grow_From_Zero_Initial_Capacity()
{
var scheduler = new CoroutineScheduler(new TestTimeSource(), initialCapacity: 0);
var handle = scheduler.Run(CreateYieldingCoroutine(new WaitOneFrame()));
Assert.That(handle.IsValid, Is.True);
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1));
}
/// <summary>
/// 验证协程调度器应该使用提供的时间源
/// </summary>
@ -563,4 +577,4 @@ public class TestTimeSource : ITimeSource
DeltaTime = 0.1;
CurrentTime += DeltaTime;
}
}
}

View File

@ -124,6 +124,24 @@ public class EventTests
Assert.That(values, Does.Contain(10));
}
/// <summary>
/// 测试单参数事件的监听器计数只统计真实注册的处理器。
/// </summary>
[Test]
public void EventT_GetListenerCount_Should_Exclude_Placeholder_Handler()
{
Assert.That(_eventInt.GetListenerCount(), Is.EqualTo(0));
Action<int> handler = _ => { };
_eventInt.Register(handler);
Assert.That(_eventInt.GetListenerCount(), Is.EqualTo(1));
_eventInt.UnRegister(handler);
Assert.That(_eventInt.GetListenerCount(), Is.EqualTo(0));
}
/// <summary>
/// 测试带两个泛型参数的事件注册功能是否正确添加处理器
/// </summary>
@ -161,4 +179,22 @@ public class EventTests
_eventIntString.Trigger(2, "b");
Assert.That(count, Is.EqualTo(1));
}
}
/// <summary>
/// 测试双参数事件的监听器计数只统计真实注册的处理器。
/// </summary>
[Test]
public void EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler()
{
Assert.That(_eventIntString.GetListenerCount(), Is.EqualTo(0));
Action<int, string> handler = (_, _) => { };
_eventIntString.Register(handler);
Assert.That(_eventIntString.GetListenerCount(), Is.EqualTo(1));
_eventIntString.UnRegister(handler);
Assert.That(_eventIntString.GetListenerCount(), Is.EqualTo(0));
}
}

View File

@ -394,6 +394,35 @@ public class PauseStackManagerTests
Assert.That(_manager.IsPaused(PauseGroup.Audio), Is.False);
}
/// <summary>
/// 验证销毁时会向所有仍暂停的组补发恢复通知。
/// </summary>
[Test]
public async Task DestroyAsync_Should_NotifyResumedGroups()
{
var resumedGroups = new List<PauseGroup>();
var mockHandler = new MockPauseHandler();
_manager.RegisterHandler(mockHandler);
_manager.OnPauseStateChanged += (_, e) =>
{
if (!e.IsPaused)
{
resumedGroups.Add(e.Group);
}
};
_manager.Push("Global", PauseGroup.Global);
_manager.Push("Gameplay", PauseGroup.Gameplay);
mockHandler.Reset();
await _manager.DestroyAsync();
Assert.That(mockHandler.CallCount, Is.EqualTo(2));
Assert.That(mockHandler.LastIsPaused, Is.False);
Assert.That(resumedGroups, Is.EquivalentTo(new[] { PauseGroup.Global, PauseGroup.Gameplay }));
}
/// <summary>
/// 验证并发Push是线程安全的
/// </summary>
@ -469,4 +498,4 @@ public class PauseStackManagerTests
LastIsPaused = null;
}
}
}
}

View File

@ -1,3 +1,6 @@
using System.Collections;
using System.Reflection;
namespace GFramework.Core.Tests.StateManagement;
/// <summary>
@ -384,6 +387,32 @@ public class StoreTests
Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" }));
}
/// <summary>
/// 测试当 dispatch 作用域在快照阶段抛出异常时Store 不会残留“正在分发”标记而锁死后续调用。
/// </summary>
[Test]
public void Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws()
{
var store = new Store<CounterState>(
new CounterState(0, "Player"),
actionMatchingMode: StoreActionMatchingMode.IncludeAssignableTypes);
store.RegisterReducer<IncrementAction>((state, action) => state with { Count = state.Count + action.Amount });
var reducers = GetReducersDictionary(store);
var throwingActionType = new ThrowingAssignableType(typeof(IncrementAction), "simulated reducer snapshot failure");
reducers.Add(throwingActionType, CreateEmptyReducerRegistrationList(reducers));
Assert.That(
() => store.Dispatch(new IncrementAction(1)),
Throws.InvalidOperationException.With.Message.EqualTo("simulated reducer snapshot failure"));
reducers.Remove(throwingActionType);
Assert.That(() => store.Dispatch(new IncrementAction(1)), Throws.Nothing);
Assert.That(store.State.Count, Is.EqualTo(1));
}
/// <summary>
/// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。
/// </summary>
@ -681,6 +710,32 @@ public class StoreTests
return store;
}
/// <summary>
/// 读取 Store 内部 reducer 字典,以便在异常安全回归测试中注入受控的异常源。
/// </summary>
/// <param name="store">要读取的 Store 实例。</param>
/// <returns>Store 当前持有的 reducer 字典引用。</returns>
private static IDictionary GetReducersDictionary(Store<CounterState> store)
{
var reducersField = typeof(Store<CounterState>).GetField("_reducers", BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException("Unable to locate Store reducer dictionary field.");
return (IDictionary)(reducersField.GetValue(store)
?? throw new InvalidOperationException("Store reducer dictionary should not be null."));
}
/// <summary>
/// 创建与 Store 私有 reducer 注册列表兼容的空列表实例。
/// </summary>
/// <param name="reducers">现有 reducer 字典,用于推断私有列表元素类型。</param>
/// <returns>可写入私有 reducer 字典的空列表。</returns>
private static object CreateEmptyReducerRegistrationList(IDictionary reducers)
{
var valueType = reducers.GetType().GenericTypeArguments[1];
return Activator.CreateInstance(valueType)
?? throw new InvalidOperationException("Unable to create an empty reducer registration list.");
}
/// <summary>
/// 用于测试的计数器状态。
/// 使用 record 保持逻辑不可变语义,便于 Store 基于状态快照进行比较和断言。
@ -876,4 +931,20 @@ public class StoreTests
next();
}
}
}
/// <summary>
/// 用于在回归测试中稳定模拟 <see cref="Type.IsAssignableFrom(Type)"/> 失败的代理类型。
/// </summary>
private sealed class ThrowingAssignableType(Type delegatingType, string message) : TypeDelegator(delegatingType)
{
/// <summary>
/// 在 Store 创建 reducer 快照时抛出受控异常,验证 dispatch 作用域能够正确回滚。
/// </summary>
/// <param name="typeInfo">待比较的 action 运行时类型。</param>
/// <returns>此实现不会正常返回。</returns>
public override bool IsAssignableFrom(Type? typeInfo)
{
throw new InvalidOperationException(message);
}
}
}

View File

@ -145,69 +145,34 @@ internal sealed class ArchitectureLifecycle(
{
logger.Info($"Initializing {_pendingInitializableList.Count} components");
// 按类型分组初始化(保持原有的阶段划分)
var utilities = _pendingInitializableList.OfType<IContextUtility>().ToList();
var models = _pendingInitializableList.OfType<IModel>().ToList();
var systems = _pendingInitializableList.OfType<ISystem>().ToList();
var initializationPlan = CreateInitializationPlan();
// 1. 工具初始化阶段
EnterPhase(ArchitecturePhase.BeforeUtilityInit);
await InitializePhaseComponentsAsync(
initializationPlan.Utilities,
ArchitecturePhase.BeforeUtilityInit,
ArchitecturePhase.AfterUtilityInit,
"context utilities",
"utility",
asyncMode)
.ConfigureAwait(false);
await InitializePhaseComponentsAsync(
initializationPlan.Models,
ArchitecturePhase.BeforeModelInit,
ArchitecturePhase.AfterModelInit,
"models",
"model",
asyncMode)
.ConfigureAwait(false);
await InitializePhaseComponentsAsync(
initializationPlan.Systems,
ArchitecturePhase.BeforeSystemInit,
ArchitecturePhase.AfterSystemInit,
"systems",
"system",
asyncMode)
.ConfigureAwait(false);
if (utilities.Count != 0)
{
logger.Info($"Initializing {utilities.Count} context utilities");
foreach (var utility in utilities)
{
logger.Debug($"Initializing utility: {utility.GetType().Name}");
await InitializeComponentAsync(utility, asyncMode).ConfigureAwait(false);
}
logger.Info("All context utilities initialized");
}
EnterPhase(ArchitecturePhase.AfterUtilityInit);
// 2. 模型初始化阶段
EnterPhase(ArchitecturePhase.BeforeModelInit);
if (models.Count != 0)
{
logger.Info($"Initializing {models.Count} models");
foreach (var model in models)
{
logger.Debug($"Initializing model: {model.GetType().Name}");
await InitializeComponentAsync(model, asyncMode).ConfigureAwait(false);
}
logger.Info("All models initialized");
}
EnterPhase(ArchitecturePhase.AfterModelInit);
// 3. 系统初始化阶段
EnterPhase(ArchitecturePhase.BeforeSystemInit);
if (systems.Count != 0)
{
logger.Info($"Initializing {systems.Count} systems");
foreach (var system in systems)
{
logger.Debug($"Initializing system: {system.GetType().Name}");
await InitializeComponentAsync(system, asyncMode).ConfigureAwait(false);
}
logger.Info("All systems initialized");
}
EnterPhase(ArchitecturePhase.AfterSystemInit);
_pendingInitializableList.Clear();
_pendingInitializableSet.Clear();
_initialized = true;
logger.Info("All components initialized");
MarkInitializationCompleted();
}
/// <summary>
@ -223,6 +188,67 @@ internal sealed class ArchitectureLifecycle(
component.Initialize();
}
/// <summary>
/// 按架构既有阶段语义把待初始化组件拆分为 utility、model 和 system 三个批次。
/// 这样可以在压缩主流程复杂度的同时,继续复用注册顺序和接口类型决定的初始化分层。
/// </summary>
/// <returns>当前待初始化组件的阶段化批次。</returns>
private InitializationPlan CreateInitializationPlan()
{
return new InitializationPlan(
_pendingInitializableList.OfType<IContextUtility>().ToList(),
_pendingInitializableList.OfType<IModel>().ToList(),
_pendingInitializableList.OfType<ISystem>().ToList());
}
/// <summary>
/// 执行单个生命周期阶段的批量初始化,并统一维护阶段切换、日志输出和异步初始化策略。
/// </summary>
/// <typeparam name="TComponent">当前阶段要初始化的组件类型。</typeparam>
/// <param name="components">当前阶段的组件列表。</param>
/// <param name="beforePhase">阶段开始前要进入的生命周期状态。</param>
/// <param name="afterPhase">阶段结束后要进入的生命周期状态。</param>
/// <param name="componentGroupName">用于批量日志的组件组名称。</param>
/// <param name="componentLogName">用于单个组件日志的组件角色名称。</param>
/// <param name="asyncMode">是否允许优先走异步初始化契约。</param>
private async Task InitializePhaseComponentsAsync<TComponent>(
IReadOnlyList<TComponent> components,
ArchitecturePhase beforePhase,
ArchitecturePhase afterPhase,
string componentGroupName,
string componentLogName,
bool asyncMode)
where TComponent : class, IInitializable
{
EnterPhase(beforePhase);
if (components.Count != 0)
{
logger.Info($"Initializing {components.Count} {componentGroupName}");
foreach (var component in components)
{
logger.Debug($"Initializing {componentLogName}: {component.GetType().Name}");
await InitializeComponentAsync(component, asyncMode).ConfigureAwait(false);
}
logger.Info($"All {componentGroupName} initialized");
}
EnterPhase(afterPhase);
}
/// <summary>
/// 在所有阶段初始化完成后清理挂起列表,并把生命周期状态切换到“已初始化”。
/// </summary>
private void MarkInitializationCompleted()
{
_pendingInitializableList.Clear();
_pendingInitializableSet.Clear();
_initialized = true;
logger.Info("All components initialized");
}
/// <summary>
/// 立即初始化在常规初始化批次完成后新增的组件。
/// 当启用 <c>AllowLateRegistration</c> 时,生命周期层需要和注册层保持一致,
@ -258,6 +284,17 @@ internal sealed class ArchitectureLifecycle(
#endregion
/// <summary>
/// 保存一次完整初始化流程所需的三个阶段批次。
/// </summary>
/// <param name="Utilities">Utility 初始化批次。</param>
/// <param name="Models">Model 初始化批次。</param>
/// <param name="Systems">System 初始化批次。</param>
private readonly record struct InitializationPlan(
IReadOnlyList<IContextUtility> Utilities,
IReadOnlyList<IModel> Models,
IReadOnlyList<ISystem> Systems);
#region Ready State
/// <summary>

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
@ -26,3 +27,54 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand
/// <returns>表示异步操作的任务</returns>
protected abstract Task OnExecuteAsync();
}
/// <summary>
/// 抽象异步命令基类,为需要命令输入且无返回值的异步命令提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">命令输入类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <param name="input">命令输入参数。</param>
public abstract class AbstractAsyncCommand<TInput>(TInput input) : ContextAwareBase, IAsyncCommand
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令的实现方法。
/// </summary>
/// <returns>表示异步操作的任务。</returns>
async Task IAsyncCommand.ExecuteAsync()
{
await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑。
/// </summary>
/// <param name="input">命令输入参数。</param>
/// <returns>表示异步操作的任务。</returns>
protected abstract Task OnExecuteAsync(TInput input);
}
/// <summary>
/// 抽象异步命令基类,为需要命令输入且返回结果的异步命令提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">命令输入类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <typeparam name="TResult">命令执行结果类型。</typeparam>
/// <param name="input">命令输入参数。</param>
public abstract class AbstractAsyncCommand<TInput, TResult>(TInput input) : ContextAwareBase, IAsyncCommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令并返回结果的实现方法。
/// </summary>
/// <returns>表示异步操作且包含结果的任务。</returns>
async Task<TResult> IAsyncCommand<TResult>.ExecuteAsync()
{
return await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑并返回结果。
/// </summary>
/// <param name="input">命令输入参数。</param>
/// <returns>表示异步操作且包含结果的任务。</returns>
protected abstract Task<TResult> OnExecuteAsync(TInput input);
}

View File

@ -1,29 +0,0 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
/// <summary>
/// 抽象异步命令基类,用于处理无返回值的异步命令操作
/// </summary>
/// <typeparam name="TInput">命令输入类型必须实现ICommandInput接口</typeparam>
public abstract class AbstractAsyncCommand<TInput>(TInput input) : ContextAwareBase, IAsyncCommand
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令的实现方法
/// </summary>
/// <returns>表示异步操作的任务</returns>
async Task IAsyncCommand.ExecuteAsync()
{
await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑
/// </summary>
/// <param name="input">命令输入参数</param>
/// <returns>表示异步操作的任务</returns>
protected abstract Task OnExecuteAsync(TInput input);
}

View File

@ -1,30 +0,0 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
/// <summary>
/// 抽象异步命令基类,用于处理有返回值的异步命令操作
/// </summary>
/// <typeparam name="TInput">命令输入类型必须实现ICommandInput接口</typeparam>
/// <typeparam name="TResult">命令执行结果类型</typeparam>
public abstract class AbstractAsyncCommand<TInput, TResult>(TInput input) : ContextAwareBase, IAsyncCommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行异步命令并返回结果的实现方法
/// </summary>
/// <returns>表示异步操作且包含结果的任务</returns>
async Task<TResult> IAsyncCommand<TResult>.ExecuteAsync()
{
return await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>
/// 定义异步执行逻辑的抽象方法,由派生类实现具体业务逻辑并返回结果
/// </summary>
/// <param name="input">命令输入参数</param>
/// <returns>表示异步操作且包含结果的任务</returns>
protected abstract Task<TResult> OnExecuteAsync(TInput input);
}

View File

@ -1,5 +1,7 @@
using GFramework.Core.Abstractions.Command;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Command;
@ -20,4 +22,55 @@ public abstract class AbstractCommand : ContextAwareBase, ICommand
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
/// </summary>
protected abstract void OnExecute();
}
}
/// <summary>
/// 抽象命令类,实现 <see cref="ICommand" /> 接口,为需要命令输入的具体命令提供基础架构支持。
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <param name="input">命令执行所需的输入参数。</param>
public abstract class AbstractCommand<TInput>(TInput input) : ContextAwareBase, ICommand
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 <see cref="ICommand" /> 接口的 <c>Execute</c> 方法。
/// </summary>
void ICommand.Execute()
{
OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑。
/// </summary>
/// <param name="input">命令执行所需的输入参数。</param>
protected abstract void OnExecute(TInput input);
}
/// <summary>
/// 带返回值的抽象命令类,为需要输入和返回值的命令提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 <see cref="ICommandInput" /> 接口。</typeparam>
/// <typeparam name="TResult">命令执行后返回的结果类型。</typeparam>
/// <param name="input">命令执行所需的输入参数。</param>
public abstract class AbstractCommand<TInput, TResult>(TInput input)
: ContextAwareBase, GFramework.Core.Abstractions.Command.ICommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 <see cref="GFramework.Core.Abstractions.Command.ICommand{TResult}" /> 接口的
/// <c>Execute</c> 方法。
/// </summary>
/// <returns>命令执行后的结果。</returns>
TResult GFramework.Core.Abstractions.Command.ICommand<TResult>.Execute()
{
return OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑。
/// </summary>
/// <param name="input">命令执行所需的输入参数。</param>
/// <returns>命令执行后的结果。</returns>
protected abstract TResult OnExecute(TInput input);
}

View File

@ -1,28 +0,0 @@
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
namespace GFramework.Core.Command;
/// <summary>
/// 抽象命令类,实现 ICommand 接口,为具体命令提供基础架构支持
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 ICommandInput 接口</typeparam>
/// <param name="input">命令执行所需的输入参数</param>
public abstract class AbstractCommand<TInput>(TInput input) : ContextAwareBase, ICommand
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 ICommand 接口的 Execute 方法
/// </summary>
void ICommand.Execute()
{
OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
/// </summary>
/// <param name="input">命令执行所需的输入参数</param>
protected abstract void OnExecute(TInput input);
}

View File

@ -1,31 +0,0 @@
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Command;
namespace GFramework.Core.Command;
/// <summary>
/// 带返回值的抽象命令类,实现 ICommand{TResult} 接口,为需要返回结果的命令提供基础架构支持
/// </summary>
/// <typeparam name="TInput">命令输入参数类型,必须实现 ICommandInput 接口</typeparam>
/// <typeparam name="TResult">命令执行后返回的结果类型</typeparam>
/// <param name="input">命令执行所需的输入参数</param>
public abstract class AbstractCommand<TInput, TResult>(TInput input)
: ContextAwareBase, Abstractions.Command.ICommand<TResult>
where TInput : ICommandInput
{
/// <summary>
/// 执行命令的入口方法,实现 ICommand{TResult} 接口的 Execute 方法
/// </summary>
/// <returns>命令执行后的结果</returns>
TResult Abstractions.Command.ICommand<TResult>.Execute()
{
return OnExecute(input);
}
/// <summary>
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
/// </summary>
/// <param name="input">命令执行所需的输入参数</param>
/// <returns>命令执行后的结果</returns>
protected abstract TResult OnExecute(TInput input);
}

View File

@ -16,7 +16,7 @@ namespace GFramework.Core.Coroutine;
/// </remarks>
/// <param name="timeSource">缩放时间源,提供调度器默认推进所使用的时间数据。</param>
/// <param name="instanceId">协程实例编号,用于生成带宿主前缀的句柄。</param>
/// <param name="initialCapacity">调度器初始槽位容量。</param>
/// <param name="initialCapacity">调度器初始槽位容量;允许为 0此时首次启动协程会按需自动扩容。</param>
/// <param name="enableStatistics">是否启用协程统计功能。</param>
/// <param name="realtimeTimeSource">
/// 非缩放时间源。
@ -211,58 +211,10 @@ public sealed class CoroutineScheduler(
return default;
}
if (_nextSlot >= _slots.Length)
{
Expand();
}
var handle = new CoroutineHandle(instanceId);
var slotIndex = _nextSlot++;
var slot = new CoroutineSlot
{
CancellationToken = cancellationToken,
Enumerator = coroutine,
State = CoroutineState.Running,
Handle = handle,
Priority = priority
};
if (cancellationToken.CanBeCanceled)
{
// 取消回调可能在任意线程触发,因此这里只做排队,真正清理由 Update 主线程完成。
slot.CancellationRegistration = cancellationToken.Register(() => _pendingKills.Enqueue(handle));
}
_slots[slotIndex] = slot;
_metadata[handle] = new CoroutineMetadata
{
ExecutionStage = executionStage,
Group = group,
Priority = priority,
SlotIndex = slotIndex,
StartTime = _timeSource.CurrentTime * 1000,
State = CoroutineState.Running,
Tag = tag
};
_completionSources[handle] =
new TaskCompletionSource<CoroutineCompletionStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
_completionStatuses.Remove(handle);
if (!string.IsNullOrEmpty(tag))
{
AddTag(tag, handle);
}
if (!string.IsNullOrEmpty(group))
{
AddGroup(group, handle);
}
_statistics?.RecordStart(priority, tag);
ActiveCoroutineCount++;
var slotIndex = AllocateSlotIndex();
var slot = CreateRunningSlot(handle, coroutine, priority, cancellationToken);
RegisterStartedCoroutine(handle, slotIndex, slot, priority, tag, group);
Prewarm(slotIndex);
UpdateStatisticsSnapshot();
@ -662,70 +614,14 @@ public sealed class CoroutineScheduler(
CoroutineCompletionStatus completionStatus,
Exception? exception = null)
{
var slot = _slots[slotIndex];
if (slot == null)
if (!TryGetFinalizableCoroutine(slotIndex, out var slot, out var handle))
{
return;
}
var handle = slot.Handle;
if (!handle.IsValid)
{
return;
}
if (_metadata.TryGetValue(handle, out var meta))
{
if (meta.State == CoroutineState.Paused && _pausedCount > 0)
{
_pausedCount--;
}
var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime;
switch (completionStatus)
{
case CoroutineCompletionStatus.Completed:
meta.State = CoroutineState.Completed;
_statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Faulted:
meta.State = CoroutineState.Completed;
_statistics?.RecordFailure(meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Cancelled:
meta.State = CoroutineState.Cancelled;
break;
default:
throw new ArgumentOutOfRangeException(
nameof(completionStatus),
completionStatus,
"Unsupported coroutine completion status.");
}
}
DisposeSlotResources(slot);
_slots[slotIndex] = null;
if (ActiveCoroutineCount > 0)
{
ActiveCoroutineCount--;
}
RemoveTag(handle);
RemoveGroup(handle);
_metadata.Remove(handle);
WakeWaiters(handle);
if (_completionSources.Remove(handle, out var source))
{
source.TrySetResult(completionStatus);
}
RecordCompletionStatus(handle, completionStatus);
UpdateCompletionMetadata(handle, completionStatus);
ReleaseCompletedCoroutine(slotIndex, slot, handle);
CompleteCoroutineLifecycle(handle, completionStatus);
OnCoroutineFinished?.Invoke(handle, completionStatus, exception);
}
@ -799,6 +695,139 @@ public sealed class CoroutineScheduler(
}
}
/// <summary>
/// 为新协程分配槽位索引,并在需要时扩容槽位数组。
/// </summary>
/// <returns>可写入的新槽位索引。</returns>
private int AllocateSlotIndex()
{
if (_nextSlot >= _slots.Length)
{
Expand();
}
return _nextSlot++;
}
/// <summary>
/// 创建处于运行态的协程槽位,并在需要时挂接跨线程取消回调。
/// </summary>
/// <param name="handle">新协程句柄。</param>
/// <param name="coroutine">协程枚举器。</param>
/// <param name="priority">协程优先级。</param>
/// <param name="cancellationToken">外部取消令牌。</param>
/// <returns>已初始化的协程槽位。</returns>
private CoroutineSlot CreateRunningSlot(
CoroutineHandle handle,
IEnumerator<IYieldInstruction> coroutine,
CoroutinePriority priority,
CancellationToken cancellationToken)
{
var slot = new CoroutineSlot
{
CancellationToken = cancellationToken,
Enumerator = coroutine,
State = CoroutineState.Running,
Handle = handle,
Priority = priority
};
RegisterCancellationCallback(slot, handle, cancellationToken);
return slot;
}
/// <summary>
/// 为支持取消的协程注册待终止排队回调。
/// </summary>
/// <param name="slot">目标协程槽位。</param>
/// <param name="handle">协程句柄。</param>
/// <param name="cancellationToken">外部取消令牌。</param>
private void RegisterCancellationCallback(
CoroutineSlot slot,
CoroutineHandle handle,
CancellationToken cancellationToken)
{
if (!cancellationToken.CanBeCanceled)
{
return;
}
// 取消回调可能在任意线程触发,因此这里只做排队,真正清理由 Update 主线程完成。
slot.CancellationRegistration = cancellationToken.Register(() => _pendingKills.Enqueue(handle));
}
/// <summary>
/// 将新协程写入调度器的槽位、元数据、标签分组和完成状态跟踪结构。
/// </summary>
/// <param name="handle">协程句柄。</param>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="slot">已初始化的协程槽位。</param>
/// <param name="priority">协程优先级。</param>
/// <param name="tag">可选标签。</param>
/// <param name="group">可选分组。</param>
private void RegisterStartedCoroutine(
CoroutineHandle handle,
int slotIndex,
CoroutineSlot slot,
CoroutinePriority priority,
string? tag,
string? group)
{
_slots[slotIndex] = slot;
_metadata[handle] = CreateCoroutineMetadata(slotIndex, priority, tag, group);
ResetCompletionTracking(handle);
if (!string.IsNullOrEmpty(tag))
{
AddTag(tag, handle);
}
if (!string.IsNullOrEmpty(group))
{
AddGroup(group, handle);
}
_statistics?.RecordStart(priority, tag);
ActiveCoroutineCount++;
}
/// <summary>
/// 创建新协程的初始元数据。
/// </summary>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="priority">协程优先级。</param>
/// <param name="tag">可选标签。</param>
/// <param name="group">可选分组。</param>
/// <returns>与新槽位对应的元数据对象。</returns>
private CoroutineMetadata CreateCoroutineMetadata(
int slotIndex,
CoroutinePriority priority,
string? tag,
string? group)
{
return new CoroutineMetadata
{
ExecutionStage = executionStage,
Group = group,
Priority = priority,
SlotIndex = slotIndex,
StartTime = _timeSource.CurrentTime * 1000,
State = CoroutineState.Running,
Tag = tag
};
}
/// <summary>
/// 重置协程完成跟踪,使复用句柄不会携带上一轮完成结果。
/// </summary>
/// <param name="handle">协程句柄。</param>
private void ResetCompletionTracking(CoroutineHandle handle)
{
_completionSources[handle] =
new TaskCompletionSource<CoroutineCompletionStatus>(TaskCreationOptions.RunContinuationsAsynchronously);
_completionStatuses.Remove(handle);
}
/// <summary>
/// 释放单个槽位持有的资源。
/// </summary>
@ -824,6 +853,125 @@ public sealed class CoroutineScheduler(
slot.Waiting = null;
}
/// <summary>
/// 读取可被完成处理的协程槽位与句柄。
/// 当槽位已空或句柄已失效时,说明该协程已经被其他路径清理,无需重复执行结束逻辑。
/// </summary>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="slot">若成功则返回槽位。</param>
/// <param name="handle">若成功则返回句柄。</param>
/// <returns>当存在可完成的协程时返回 <see langword="true" />。</returns>
private bool TryGetFinalizableCoroutine(int slotIndex, out CoroutineSlot slot, out CoroutineHandle handle)
{
var candidate = _slots[slotIndex];
if (candidate == null)
{
slot = null!;
handle = default;
return false;
}
handle = candidate.Handle;
if (!handle.IsValid)
{
slot = null!;
return false;
}
slot = candidate;
return true;
}
/// <summary>
/// 根据最终状态更新协程元数据与统计信息。
/// </summary>
/// <param name="handle">协程句柄。</param>
/// <param name="completionStatus">最终结果。</param>
private void UpdateCompletionMetadata(CoroutineHandle handle, CoroutineCompletionStatus completionStatus)
{
if (!_metadata.TryGetValue(handle, out var meta))
{
return;
}
if (meta.State == CoroutineState.Paused && _pausedCount > 0)
{
_pausedCount--;
}
ApplyCompletionMetadata(meta, completionStatus);
}
/// <summary>
/// 将最终结果映射到元数据状态和统计记录。
/// </summary>
/// <param name="meta">协程元数据。</param>
/// <param name="completionStatus">最终结果。</param>
private void ApplyCompletionMetadata(CoroutineMetadata meta, CoroutineCompletionStatus completionStatus)
{
var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime;
switch (completionStatus)
{
case CoroutineCompletionStatus.Completed:
meta.State = CoroutineState.Completed;
_statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Faulted:
meta.State = CoroutineState.Completed;
_statistics?.RecordFailure(meta.Priority, meta.Tag);
break;
case CoroutineCompletionStatus.Cancelled:
meta.State = CoroutineState.Cancelled;
break;
default:
throw new ArgumentOutOfRangeException(
nameof(completionStatus),
completionStatus,
"Unsupported coroutine completion status.");
}
}
/// <summary>
/// 释放已结束协程占用的槽位和索引结构。
/// </summary>
/// <param name="slotIndex">槽位索引。</param>
/// <param name="slot">已结束的协程槽位。</param>
/// <param name="handle">协程句柄。</param>
private void ReleaseCompletedCoroutine(int slotIndex, CoroutineSlot slot, CoroutineHandle handle)
{
DisposeSlotResources(slot);
_slots[slotIndex] = null;
if (ActiveCoroutineCount > 0)
{
ActiveCoroutineCount--;
}
RemoveTag(handle);
RemoveGroup(handle);
_metadata.Remove(handle);
}
/// <summary>
/// 完成协程的等待者唤醒、任务结果和完成历史记录。
/// </summary>
/// <param name="handle">协程句柄。</param>
/// <param name="completionStatus">最终结果。</param>
private void CompleteCoroutineLifecycle(CoroutineHandle handle, CoroutineCompletionStatus completionStatus)
{
WakeWaiters(handle);
if (_completionSources.Remove(handle, out var source))
{
source.TrySetResult(completionStatus);
}
RecordCompletionStatus(handle, completionStatus);
}
/// <summary>
/// 唤醒所有等待目标协程完成的协程。
/// </summary>
@ -888,7 +1036,9 @@ public sealed class CoroutineScheduler(
/// </summary>
private void Expand()
{
Array.Resize(ref _slots, _slots.Length * 2);
// 允许构造器以 0 容量启动,用于极简场景或测试;首次分配时至少扩到 1避免后续写槽位越界。
var expandedLength = Math.Max(1, _slots.Length * 2);
Array.Resize(ref _slots, expandedLength);
}
/// <summary>

View File

@ -3,24 +3,24 @@
namespace GFramework.Core.Events;
/// <summary>
/// 泛型事件类,支持一个泛型参数 T 的事件注册、注销与触发。
/// 实现了 IEvent 接口以提供统一的事件操作接口。
/// 泛型事件类,支持一个泛型参数 <typeparamref name="T" /> 的事件注册、注销与触发。
/// 实现了 <see cref="IEvent" /> 接口以提供统一的事件操作接口。
/// </summary>
/// <typeparam name="T">事件回调函数的第一个参数类型。</typeparam>
public class Event<T> : IEvent
{
/// <summary>
/// 存储已注册的事件处理委托。
/// 默认为空操作no-op委托避免 null 检查
/// 未注册监听器时保持 <see langword="null" />,从而让监听器计数与真实订阅数量保持一致
/// </summary>
private Action<T>? _mOnEvent = _ => { };
private Action<T>? _mOnEvent;
/// <summary>
/// 显式实现 IEvent 接口中的 Register 方法。
/// 允许使用无参 Action 来订阅当前带参事件。
/// 显式实现 <see cref="IEvent" /> 接口中的 <c>Register</c> 方法。
/// 允许使用无参 <see cref="Action" /> 来订阅当前带参事件。
/// </summary>
/// <param name="onEvent">无参事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
IUnRegister IEvent.Register(Action onEvent)
{
return Register(Action);
@ -35,7 +35,7 @@ public class Event<T> : IEvent
/// 注册一个事件监听器,并返回可用于取消注册的对象。
/// </summary>
/// <param name="onEvent">要注册的事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
public IUnRegister Register(Action<T> onEvent)
{
_mOnEvent += onEvent;
@ -52,7 +52,7 @@ public class Event<T> : IEvent
}
/// <summary>
/// 触发所有已注册的事件处理程序,并传递参数 t。
/// 触发所有已注册的事件处理程序,并传递参数 <paramref name="t" />
/// </summary>
/// <param name="t">传递给事件处理程序的参数。</param>
public void Trigger(T t)
@ -61,9 +61,9 @@ public class Event<T> : IEvent
}
/// <summary>
/// 获取当前已注册的监听器数量
/// 获取当前已注册的监听器数量
/// </summary>
/// <returns>监听器数量</returns>
/// <returns>监听器数量</returns>
public int GetListenerCount()
{
return _mOnEvent?.GetInvocationList().Length ?? 0;
@ -71,30 +71,30 @@ public class Event<T> : IEvent
}
/// <summary>
/// 支持两个泛型参数 T 和 TK 的事件类。
/// 支持两个泛型参数 <typeparamref name="T" /><typeparamref name="TK" /> 的事件类。
/// 提供事件注册、注销和触发功能。
/// </summary>
/// <typeparam name="T">第一个参数类型。</typeparam>
/// <typeparam name="Tk">第二个参数类型。</typeparam>
public class Event<T, Tk> : IEvent
/// <typeparam name="TK">第二个参数类型。</typeparam>
public class Event<T, TK> : IEvent
{
/// <summary>
/// 存储已注册的双参数事件处理委托。
/// 默认为空操作no-op委托
/// 未注册监听器时保持 <see langword="null" />,从而让监听器计数与真实订阅数量保持一致
/// </summary>
private Action<T, Tk>? _mOnEvent = (_, _) => { };
private Action<T, TK>? _mOnEvent;
/// <summary>
/// 显式实现 IEvent 接口中的 Register 方法。
/// 允许使用无参 Action 来订阅当前带参事件。
/// 显式实现 <see cref="IEvent" /> 接口中的 <c>Register</c> 方法。
/// 允许使用无参 <see cref="Action" /> 来订阅当前带参事件。
/// </summary>
/// <param name="onEvent">无参事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
IUnRegister IEvent.Register(Action onEvent)
{
return Register(Action);
void Action(T _, Tk __)
void Action(T _, TK __)
{
onEvent();
}
@ -104,8 +104,8 @@ public class Event<T, Tk> : IEvent
/// 注册一个接受两个参数的事件监听器,并返回可用于取消注册的对象。
/// </summary>
/// <param name="onEvent">要注册的事件处理方法。</param>
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
public IUnRegister Register(Action<T, Tk> onEvent)
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
public IUnRegister Register(Action<T, TK> onEvent)
{
_mOnEvent += onEvent;
return new DefaultUnRegister(() => UnRegister(onEvent));
@ -115,27 +115,27 @@ public class Event<T, Tk> : IEvent
/// 取消指定的双参数事件监听器。
/// </summary>
/// <param name="onEvent">需要被注销的事件处理方法。</param>
public void UnRegister(Action<T, Tk> onEvent)
public void UnRegister(Action<T, TK> onEvent)
{
_mOnEvent -= onEvent;
}
/// <summary>
/// 触发所有已注册的事件处理程序,并传递参数 t 和 k。
/// 触发所有已注册的事件处理程序,并传递参数 <paramref name="t" /><paramref name="k" />
/// </summary>
/// <param name="t">第一个参数。</param>
/// <param name="k">第二个参数。</param>
public void Trigger(T t, Tk k)
public void Trigger(T t, TK k)
{
_mOnEvent?.Invoke(t, k);
}
/// <summary>
/// 获取当前已注册的监听器数量
/// 获取当前已注册的监听器数量
/// </summary>
/// <returns>监听器数量</returns>
/// <returns>监听器数量</returns>
public int GetListenerCount()
{
return _mOnEvent?.GetInvocationList().Length ?? 0;
}
}
}

View File

@ -26,69 +26,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
public ValueTask DestroyAsync()
{
if (_disposed)
{
return ValueTask.CompletedTask;
List<PauseGroup> pausedGroups;
IPauseHandler[] handlersSnapshot;
_lock.EnterWriteLock();
try
{
if (_disposed)
return ValueTask.CompletedTask;
_disposed = true;
// 收集所有当前暂停的组
pausedGroups = _pauseStacks
.Where(kvp => kvp.Value.Count > 0)
.Select(kvp => kvp.Key)
.ToList();
// 获取处理器快照
handlersSnapshot = _handlers.ToArray();
// 清理所有数据结构
_pauseStacks.Clear();
_tokenMap.Clear();
_handlers.Clear();
_logger.Debug("PauseStackManager destroyed");
}
finally
{
_lock.ExitWriteLock();
}
// 在锁外通知所有之前暂停的组恢复,保持生命周期信号一致
foreach (var group in pausedGroups)
var destroySnapshot = TryBeginDestroy();
if (destroySnapshot == null)
{
_logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false");
foreach (var handler in handlersSnapshot.OrderBy(h => h.Priority))
{
try
{
handler.OnPauseStateChanged(group, false);
}
catch (Exception ex)
{
_logger.Error($"Handler {handler.GetType().Name} failed during destruction", ex);
}
}
// 触发事件
try
{
RaisePauseStateChanged(group, false);
}
catch (Exception ex)
{
_logger.Error($"Event subscriber failed during destruction for group {group}", ex);
}
return ValueTask.CompletedTask;
}
// 释放锁资源
NotifyDestroyedGroups(destroySnapshot.Value);
_lock.Dispose();
return ValueTask.CompletedTask;
@ -163,74 +111,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
public bool Pop(PauseToken token)
{
if (!token.IsValid)
{
return false;
bool found;
bool shouldNotify = false;
PauseGroup notifyGroup = PauseGroup.Global;
_lock.EnterWriteLock();
try
{
ThrowIfDisposed();
if (!_tokenMap.TryGetValue(token.Id, out var entry))
{
_logger.Warn($"Attempted to pop invalid/expired token: {token.Id}");
return false;
}
var group = entry.Group;
var stack = _pauseStacks[group];
var wasPaused = stack.Count > 0;
// 从栈中移除
var tempStack = new Stack<PauseEntry>();
found = false;
while (stack.Count > 0)
{
var current = stack.Pop();
if (current.TokenId == token.Id)
{
found = true;
break;
}
tempStack.Push(current);
}
// 恢复栈结构
while (tempStack.Count > 0)
{
stack.Push(tempStack.Pop());
}
if (found)
{
_tokenMap.Remove(token.Id);
_logger.Debug($"Pause popped: {entry.Reason} (Group: {group}, Remaining: {stack.Count})");
// 状态变化检测:从暂停 → 未暂停
if (wasPaused && stack.Count == 0)
{
shouldNotify = true;
notifyGroup = group;
}
}
}
finally
{
_lock.ExitWriteLock();
}
// 在锁外通知处理器,避免死锁
if (shouldNotify)
var result = TryPopEntry(token);
if (result.ShouldNotify)
{
NotifyHandlers(notifyGroup, false);
NotifyHandlers(result.NotifyGroup, false);
}
return found;
return result.Found;
}
/// <summary>
@ -443,6 +334,200 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
}
}
/// <summary>
/// 采集销毁所需的快照并清空内部状态。
/// </summary>
/// <returns>
/// 成功进入销毁阶段时返回销毁快照;如果其他线程已先完成销毁,则返回 <see langword="null" />。
/// </returns>
/// <remarks>
/// 该方法只负责锁内状态迁移,把外部回调与事件派发留到锁外执行,
/// 以避免在生命周期结束阶段持锁调用用户代码。
/// </remarks>
private DestroySnapshot? TryBeginDestroy()
{
_lock.EnterWriteLock();
try
{
if (_disposed)
{
return null;
}
_disposed = true;
var pausedGroups = CollectPausedGroups();
var handlersSnapshot = CreateHandlerSnapshot();
_pauseStacks.Clear();
_tokenMap.Clear();
_handlers.Clear();
_logger.Debug("PauseStackManager destroyed");
return new DestroySnapshot(pausedGroups, handlersSnapshot);
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 在销毁后向所有先前处于暂停状态的分组补发恢复通知。
/// </summary>
/// <param name="destroySnapshot">销毁阶段采集的分组与处理器快照。</param>
private void NotifyDestroyedGroups(DestroySnapshot destroySnapshot)
{
foreach (var group in destroySnapshot.PausedGroups)
{
_logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false");
NotifyHandlersSnapshot(group, false, destroySnapshot.HandlersSnapshot, isDestroying: true);
RaiseDestroyStateChanged(group);
}
}
/// <summary>
/// 在锁内执行令牌移除,并返回锁外通知所需的信息。
/// </summary>
/// <param name="token">要移除的暂停令牌。</param>
/// <returns>包含本次弹出结果和后续通知决策的快照。</returns>
/// <remarks>
/// Pop 支持移除非栈顶令牌,因此这里会先临时转移栈元素,再恢复原有顺序,
/// 只在最后一个暂停请求被移除时触发恢复通知。
/// </remarks>
private PopResult TryPopEntry(PauseToken token)
{
_lock.EnterWriteLock();
try
{
ThrowIfDisposed();
if (!_tokenMap.TryGetValue(token.Id, out var entry))
{
_logger.Warn($"Attempted to pop invalid/expired token: {token.Id}");
return PopResult.NotFound;
}
var stack = _pauseStacks[entry.Group];
var wasPaused = stack.Count > 0;
var found = RemoveEntryFromStack(stack, token.Id);
if (!found)
{
return PopResult.NotFound;
}
_tokenMap.Remove(token.Id);
_logger.Debug($"Pause popped: {entry.Reason} (Group: {entry.Group}, Remaining: {stack.Count})");
return new PopResult(true, wasPaused && stack.Count == 0, entry.Group);
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 从指定暂停栈中移除目标令牌,并保持其他暂停请求的原始顺序。
/// </summary>
/// <param name="stack">要修改的暂停栈。</param>
/// <param name="tokenId">目标令牌标识。</param>
/// <returns>如果找到了目标令牌则返回 <see langword="true" />。</returns>
private static bool RemoveEntryFromStack(Stack<PauseEntry> stack, Guid tokenId)
{
var tempStack = new Stack<PauseEntry>();
var found = false;
while (stack.Count > 0)
{
var current = stack.Pop();
if (current.TokenId == tokenId)
{
found = true;
break;
}
tempStack.Push(current);
}
while (tempStack.Count > 0)
{
stack.Push(tempStack.Pop());
}
return found;
}
/// <summary>
/// 收集当前仍处于暂停状态的分组列表。
/// </summary>
/// <returns>包含所有暂停中的分组的数组。</returns>
private PauseGroup[] CollectPausedGroups()
{
return _pauseStacks
.Where(kvp => kvp.Value.Count > 0)
.Select(kvp => kvp.Key)
.ToArray();
}
/// <summary>
/// 按优先级创建处理器快照,确保锁外通知仍保持确定性顺序。
/// </summary>
/// <returns>已按优先级排序的处理器快照。</returns>
private IPauseHandler[] CreateHandlerSnapshot()
{
return _handlers
.OrderBy(handler => handler.Priority)
.ToArray();
}
/// <summary>
/// 统一使用给定的处理器快照派发暂停状态变化通知。
/// </summary>
/// <param name="group">发生状态变化的暂停组。</param>
/// <param name="isPaused">新的暂停状态。</param>
/// <param name="handlersSnapshot">要通知的处理器快照。</param>
/// <param name="isDestroying">是否处于销毁补发路径。</param>
private void NotifyHandlersSnapshot(
PauseGroup group,
bool isPaused,
IReadOnlyList<IPauseHandler> handlersSnapshot,
bool isDestroying)
{
foreach (var handler in handlersSnapshot)
{
try
{
handler.OnPauseStateChanged(group, isPaused);
}
catch (Exception ex)
{
var message = isDestroying
? $"Handler {handler.GetType().Name} failed during destruction"
: $"Handler {handler.GetType().Name} failed";
_logger.Error(message, ex);
}
}
}
/// <summary>
/// 在销毁路径中独立保护事件通知,避免订阅方异常中断其他分组的恢复信号。
/// </summary>
/// <param name="group">需要补发恢复事件的暂停组。</param>
private void RaiseDestroyStateChanged(PauseGroup group)
{
try
{
RaisePauseStateChanged(group, false);
}
catch (Exception ex)
{
_logger.Error($"Event subscriber failed during destruction for group {group}", ex);
}
}
/// <summary>
/// 内部查询暂停状态的方法,不加锁。
/// </summary>
@ -467,7 +552,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
_lock.EnterReadLock();
try
{
handlersSnapshot = _handlers.OrderBy(h => h.Priority).ToArray();
handlersSnapshot = CreateHandlerSnapshot();
}
finally
{
@ -475,17 +560,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
}
// 在锁外遍历快照并通知处理器
foreach (var handler in handlersSnapshot)
{
try
{
handler.OnPauseStateChanged(group, isPaused);
}
catch (Exception ex)
{
_logger.Error($"Handler {handler.GetType().Name} failed", ex);
}
}
NotifyHandlersSnapshot(group, isPaused, handlersSnapshot, isDestroying: false);
// 触发事件
RaisePauseStateChanged(group, isPaused);
@ -508,4 +583,25 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
protected override void OnInit()
{
}
}
/// <summary>
/// 锁内采集的销毁快照,供锁外补发恢复通知使用。
/// </summary>
/// <param name="PausedGroups">销毁前仍处于暂停状态的分组。</param>
/// <param name="HandlersSnapshot">按优先级排序后的处理器快照。</param>
private readonly record struct DestroySnapshot(PauseGroup[] PausedGroups, IPauseHandler[] HandlersSnapshot);
/// <summary>
/// Pop 操作的锁内结果快照。
/// </summary>
/// <param name="Found">是否成功移除了目标令牌。</param>
/// <param name="ShouldNotify">是否需要在锁外发出恢复通知。</param>
/// <param name="NotifyGroup">需要通知的暂停组。</param>
private readonly record struct PopResult(bool Found, bool ShouldNotify, PauseGroup NotifyGroup)
{
/// <summary>
/// 表示未找到目标令牌时的默认结果。
/// </summary>
public static PopResult NotFound { get; } = new(false, false, PauseGroup.Global);
}
}

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
@ -24,4 +25,31 @@ public abstract class AbstractAsyncQuery<TResult> : ContextAwareBase, IAsyncQuer
/// </summary>
/// <returns>返回查询结果的异步任务</returns>
protected abstract Task<TResult> OnDoAsync();
}
}
/// <summary>
/// 抽象异步查询基类,为需要输入参数的异步查询提供统一执行骨架。
/// </summary>
/// <typeparam name="TInput">查询输入类型,必须实现 <see cref="IQueryInput" /> 接口。</typeparam>
/// <typeparam name="TResult">查询结果类型。</typeparam>
/// <param name="input">查询输入参数。</param>
public abstract class AbstractAsyncQuery<TInput, TResult>(TInput input)
: ContextAwareBase, IAsyncQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行异步查询操作。
/// </summary>
/// <returns>返回查询结果的异步任务。</returns>
public Task<TResult> DoAsync()
{
return OnDoAsync(input);
}
/// <summary>
/// 抽象方法,用于实现具体的异步查询逻辑。
/// </summary>
/// <param name="input">查询输入参数。</param>
/// <returns>返回查询结果的异步任务。</returns>
protected abstract Task<TResult> OnDoAsync(TInput input);
}

View File

@ -1,33 +0,0 @@
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
/// <summary>
/// 抽象异步查询基类用于处理输入类型为TInput、结果类型为TResult的异步查询操作
/// </summary>
/// <typeparam name="TInput">查询输入类型必须实现IQueryInput接口</typeparam>
/// <typeparam name="TResult">查询结果类型</typeparam>
/// <param name="input">查询输入参数</param>
public abstract class AbstractAsyncQuery<TInput, TResult>(
TInput input
) : ContextAwareBase, IAsyncQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行异步查询操作
/// </summary>
/// <returns>返回查询结果的异步任务</returns>
public Task<TResult> DoAsync()
{
return OnDoAsync(input);
}
/// <summary>
/// 抽象方法,用于实现具体的异步查询逻辑
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>返回查询结果的异步任务</returns>
protected abstract Task<TResult> OnDoAsync(TInput input);
}

View File

@ -1,5 +1,6 @@
using GFramework.Core.Abstractions.Query;
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
@ -7,7 +8,7 @@ namespace GFramework.Core.Query;
/// 抽象查询类,提供查询操作的基础实现
/// </summary>
/// <typeparam name="TResult">查询结果的类型</typeparam>
public abstract class AbstractQuery<TResult> : ContextAwareBase, IQuery<TResult>
public abstract class AbstractQuery<TResult> : ContextAwareBase, GFramework.Core.Abstractions.Query.IQuery<TResult>
{
/// <summary>
@ -25,4 +26,31 @@ public abstract class AbstractQuery<TResult> : ContextAwareBase, IQuery<TResult>
/// </summary>
/// <returns>查询结果类型为TResult</returns>
protected abstract TResult OnDo();
}
}
/// <summary>
/// 抽象查询类,为需要输入参数的同步查询提供基础实现。
/// </summary>
/// <typeparam name="TInput">查询输入参数的类型,必须实现 <see cref="IQueryInput" /> 接口。</typeparam>
/// <typeparam name="TResult">查询结果的类型。</typeparam>
/// <param name="input">查询输入参数。</param>
public abstract class AbstractQuery<TInput, TResult>(TInput input)
: ContextAwareBase, GFramework.Core.Abstractions.Query.IQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行查询操作。
/// </summary>
/// <returns>查询结果,类型为 <typeparamref name="TResult" />。</returns>
public TResult Do()
{
return OnDo(input);
}
/// <summary>
/// 抽象方法,用于实现具体的查询逻辑。
/// </summary>
/// <param name="input">查询输入参数。</param>
/// <returns>查询结果。</returns>
protected abstract TResult OnDo(TInput input);
}

View File

@ -1,30 +0,0 @@
using GFramework.Core.Rule;
using GFramework.Cqrs.Abstractions.Cqrs.Query;
namespace GFramework.Core.Query;
/// <summary>
/// 抽象查询类,提供查询操作的基础实现
/// </summary>
/// <typeparam name="TInput">查询输入参数的类型必须实现IQueryInput接口</typeparam>
/// <typeparam name="TResult">查询结果的类型</typeparam>
public abstract class AbstractQuery<TInput, TResult>(TInput input)
: ContextAwareBase, Abstractions.Query.IQuery<TResult>
where TInput : IQueryInput
{
/// <summary>
/// 执行查询操作
/// </summary>
/// <returns>查询结果类型为TResult</returns>
public TResult Do()
{
return OnDo(input);
}
/// <summary>
/// 抽象方法,用于实现具体的查询逻辑
/// </summary>
/// <param name="input">查询输入参数</param>
/// <returns>查询结果类型为TResult</returns>
protected abstract TResult OnDo(TInput input);
}

View File

@ -212,10 +212,6 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
ArgumentNullException.ThrowIfNull(action);
Action<TState>[] listenersSnapshot = Array.Empty<Action<TState>>();
IStoreMiddleware<TState>[] middlewaresSnapshot = Array.Empty<IStoreMiddleware<TState>>();
IStoreReducerAdapter[] reducersSnapshot = Array.Empty<IStoreReducerAdapter>();
IEqualityComparer<TState> stateComparerSnapshot = _stateComparer;
StoreDispatchContext<TState>? context = null;
TState notificationState = default!;
var hasNotification = false;
var enteredDispatchScope = false;
@ -224,49 +220,25 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
{
try
{
lock (_lock)
{
EnsureNotDispatching();
_isDispatching = true;
enteredDispatchScope = true;
context = new StoreDispatchContext<TState>(action!, _state);
stateComparerSnapshot = _stateComparer;
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
}
var context = EnterDispatchScope(
action,
out var middlewaresSnapshot,
out var reducersSnapshot,
out var stateComparerSnapshot);
enteredDispatchScope = true;
// middleware 和 reducer 可能包含较重的同步逻辑,因此仅持有 dispatch 串行门,
// 不占用状态锁,让读取、订阅和注册操作只在需要访问共享状态时短暂阻塞。
ExecuteDispatchPipeline(context, middlewaresSnapshot, reducersSnapshot, stateComparerSnapshot);
lock (_lock)
{
_lastActionType = context.ActionType;
_lastDispatchRecord = new StoreDispatchRecord<TState>(
context.Action,
context.PreviousState,
context.NextState,
context.HasStateChanged,
context.DispatchedAt);
if (!context.HasStateChanged)
{
return;
}
ApplyCommittedStateChange(context.NextState, context.DispatchedAt, context.Action);
listenersSnapshot = CaptureListenersOrDeferNotification(context.NextState, out notificationState);
hasNotification = listenersSnapshot.Length > 0;
}
hasNotification = TryCommitDispatchResult(context, out listenersSnapshot, out notificationState);
}
finally
{
if (enteredDispatchScope)
{
lock (_lock)
{
_isDispatching = false;
}
ExitDispatchScope();
}
}
}
@ -831,6 +803,99 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
context.HasStateChanged = !stateComparer.Equals(context.PreviousState, nextState);
}
/// <summary>
/// 进入一次新的 dispatch 作用域,并在状态锁内抓取本次执行所需的上下文快照。
/// 该方法只做最短路径的共享状态访问,把 middleware/reducer 的实际执行留到锁外完成。
/// </summary>
/// <typeparam name="TAction">action 的具体类型。</typeparam>
/// <param name="action">本次分发的 action。</param>
/// <param name="middlewaresSnapshot">返回本次 dispatch 使用的中间件快照。</param>
/// <param name="reducersSnapshot">返回本次 dispatch 使用的 reducer 快照。</param>
/// <param name="stateComparerSnapshot">返回本次 dispatch 使用的状态比较器快照。</param>
/// <returns>已初始化的 dispatch 上下文。</returns>
private StoreDispatchContext<TState> EnterDispatchScope<TAction>(
TAction action,
out IStoreMiddleware<TState>[] middlewaresSnapshot,
out IStoreReducerAdapter[] reducersSnapshot,
out IEqualityComparer<TState> stateComparerSnapshot)
{
Debug.Assert(
Monitor.IsEntered(_dispatchGate),
"Caller must hold _dispatchGate before entering a dispatch scope.");
lock (_lock)
{
EnsureNotDispatching();
_isDispatching = true;
try
{
var context = new StoreDispatchContext<TState>(action!, _state);
stateComparerSnapshot = _stateComparer;
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
return context;
}
catch
{
// 进入 dispatch 标记早于快照构建;如果这里抛异常,必须同步回滚标记,避免后续调用被永久判定为嵌套分发。
_isDispatching = false;
throw;
}
}
}
/// <summary>
/// 在 dispatch 管线执行完成后提交诊断信息和状态变更。
/// 状态与订阅集合的更新统一在该阶段完成,从而保证 dispatch 与 time-travel 共享同一提交流程。
/// </summary>
/// <param name="context">刚完成 middleware/reducer 管线的 dispatch 上下文。</param>
/// <param name="listenersSnapshot">若需要立即通知,则返回锁外回放的监听器快照。</param>
/// <param name="notificationState">若需要立即通知,则返回要通知的状态。</param>
/// <returns>本次 dispatch 是否需要在锁外执行监听器通知。</returns>
private bool TryCommitDispatchResult(
StoreDispatchContext<TState> context,
out Action<TState>[] listenersSnapshot,
out TState notificationState)
{
Debug.Assert(
Monitor.IsEntered(_dispatchGate),
"Caller must hold _dispatchGate before committing a dispatch result.");
lock (_lock)
{
_lastActionType = context.ActionType;
_lastDispatchRecord = new StoreDispatchRecord<TState>(
context.Action,
context.PreviousState,
context.NextState,
context.HasStateChanged,
context.DispatchedAt);
if (!context.HasStateChanged)
{
listenersSnapshot = Array.Empty<Action<TState>>();
notificationState = default!;
return false;
}
ApplyCommittedStateChange(context.NextState, context.DispatchedAt, context.Action);
listenersSnapshot = CaptureListenersOrDeferNotification(context.NextState, out notificationState);
return listenersSnapshot.Length > 0;
}
}
/// <summary>
/// 退出当前 dispatch 作用域,允许后续 dispatch 或历史控制继续进入。
/// </summary>
private void ExitDispatchScope()
{
lock (_lock)
{
_isDispatching = false;
}
}
/// <summary>
/// 确保当前 Store 没有发生重入分发或在 dispatch 中执行历史控制。
/// </summary>
@ -949,20 +1014,65 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
if (_actionMatchingMode == StoreActionMatchingMode.ExactTypeOnly)
{
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
for (var i = 0; i < exactReducers.Count; i++)
{
exactSnapshot[i] = exactReducers[i].Adapter;
}
return exactSnapshot;
return CreateExactReducerSnapshot(actionType);
}
return CreateAssignableReducerSnapshot(actionType);
}
/// <summary>
/// 为精确类型匹配模式创建 reducer 快照。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>精确匹配到的 reducer 快照;若未注册则返回空数组。</returns>
private IStoreReducerAdapter[] CreateExactReducerSnapshot(Type actionType)
{
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
for (var i = 0; i < exactReducers.Count; i++)
{
exactSnapshot[i] = exactReducers[i].Adapter;
}
return exactSnapshot;
}
/// <summary>
/// 为多态匹配模式创建 reducer 快照。
/// 该路径会收集所有可赋值的注册桶,并按“精确类型 -> 基类距离 -> 接口 -> 注册顺序”的稳定规则排序。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>多态模式下的 reducer 快照;若未注册则返回空数组。</returns>
private IStoreReducerAdapter[] CreateAssignableReducerSnapshot(Type actionType)
{
var matches = CollectReducerMatches(actionType);
if (matches is null || matches.Count == 0)
{
return Array.Empty<IStoreReducerAdapter>();
}
matches.Sort(CompareReducerMatch);
var snapshot = new IStoreReducerAdapter[matches.Count];
for (var i = 0; i < matches.Count; i++)
{
snapshot[i] = matches[i].Adapter;
}
return snapshot;
}
/// <summary>
/// 收集当前 action 类型可命中的 reducer 注册,并附带稳定排序所需的匹配元数据。
/// </summary>
/// <param name="actionType">当前 action 的运行时类型。</param>
/// <returns>匹配结果列表;若没有任何匹配则返回 <see langword="null"/>。</returns>
private List<ReducerMatch>? CollectReducerMatches(Type actionType)
{
List<ReducerMatch>? matches = null;
foreach (var reducerBucket in _reducers)
@ -984,35 +1094,30 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
}
}
if (matches is null || matches.Count == 0)
return matches;
}
/// <summary>
/// 比较两个 reducer 匹配结果的优先级,保证多态匹配下的执行顺序稳定可预测。
/// </summary>
/// <param name="left">左侧匹配结果。</param>
/// <param name="right">右侧匹配结果。</param>
/// <returns>排序比较结果。</returns>
private static int CompareReducerMatch(ReducerMatch left, ReducerMatch right)
{
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
if (categoryComparison != 0)
{
return Array.Empty<IStoreReducerAdapter>();
return categoryComparison;
}
matches.Sort(static (left, right) =>
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
if (distanceComparison != 0)
{
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
if (categoryComparison != 0)
{
return categoryComparison;
}
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
if (distanceComparison != 0)
{
return distanceComparison;
}
return left.Sequence.CompareTo(right.Sequence);
});
var snapshot = new IStoreReducerAdapter[matches.Count];
for (var i = 0; i < matches.Count; i++)
{
snapshot[i] = matches[i].Adapter;
return distanceComparison;
}
return snapshot;
return left.Sequence.CompareTo(right.Sequence);
}
/// <summary>
@ -1412,4 +1517,4 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
/// </summary>
public TState PendingState { get; set; } = default!;
}
}
}

View File

@ -1,6 +1,7 @@
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using System.Diagnostics.CodeAnalysis;
using System.Reflection.Emit;
namespace GFramework.Cqrs.Internal;
@ -88,63 +89,14 @@ internal static class CqrsHandlerRegistrar
if (registryTypes.Count == 0)
return GeneratedRegistrationResult.NoGeneratedRegistry();
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
foreach (var registryType in registryTypes)
{
var activationMetadata = RegistryActivationMetadataCache.GetOrAdd(
registryType,
AnalyzeRegistryActivation);
if (!TryCreateGeneratedRegistries(registryTypes, assemblyName, logger, out var registries))
return GeneratedRegistrationResult.NoGeneratedRegistry();
if (!activationMetadata.ImplementsRegistryContract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
return GeneratedRegistrationResult.NoGeneratedRegistry();
}
if (activationMetadata.IsAbstract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
return GeneratedRegistrationResult.NoGeneratedRegistry();
}
if (activationMetadata.Factory is null)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor.");
return GeneratedRegistrationResult.NoGeneratedRegistry();
}
var registry = activationMetadata.Factory();
registries.Add(registry);
}
foreach (var registry in registries)
{
logger.Debug(
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
registry.Register(services, logger);
}
var reflectionFallbackMetadata = assemblyMetadata.ReflectionFallbackMetadata;
if (reflectionFallbackMetadata is not null)
{
if (reflectionFallbackMetadata.HasExplicitTypes)
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s).");
}
else
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers.");
}
return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata);
}
return GeneratedRegistrationResult.FullyHandled();
RegisterGeneratedRegistries(services, registries, assemblyName, logger);
return BuildGeneratedRegistrationResult(
assemblyMetadata.ReflectionFallbackMetadata,
assemblyName,
logger);
}
catch (Exception exception)
{
@ -186,12 +138,138 @@ internal static class CqrsHandlerRegistrar
// Request/notification handlers receive context injection before every dispatch.
// Transient registration avoids sharing mutable Context across concurrent requests.
services.AddTransient(handlerInterface, implementationType);
logger.Debug(
logger.Debug(
$"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}.");
}
}
}
/// <summary>
/// 激活当前程序集声明的所有 generated registry若任一 registry 不满足运行时契约,则整批回退到反射扫描。
/// </summary>
/// <param name="registryTypes">程序集声明的 generated registry 类型列表。</param>
/// <param name="assemblyName">用于诊断的程序集稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <param name="registries">成功激活后的 registry 实例。</param>
/// <returns>当全部 registry 都可安全激活时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryCreateGeneratedRegistries(
IReadOnlyList<Type> registryTypes,
string assemblyName,
ILogger logger,
out IReadOnlyList<ICqrsHandlerRegistry> registries)
{
var activatedRegistries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
foreach (var registryType in registryTypes)
{
if (!TryCreateGeneratedRegistry(registryType, assemblyName, logger, out var registry))
{
registries = Array.Empty<ICqrsHandlerRegistry>();
return false;
}
activatedRegistries.Add(registry);
}
registries = activatedRegistries;
return true;
}
/// <summary>
/// 激活单个 generated registry并在契约不满足时输出与原先完全一致的回退诊断。
/// </summary>
/// <param name="registryType">要分析的 generated registry 类型。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <param name="registry">激活成功后的 registry 实例。</param>
/// <returns>当 registry 可安全使用时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
private static bool TryCreateGeneratedRegistry(
Type registryType,
string assemblyName,
ILogger logger,
[NotNullWhen(true)] out ICqrsHandlerRegistry? registry)
{
var activationMetadata = RegistryActivationMetadataCache.GetOrAdd(
registryType,
AnalyzeRegistryActivation);
if (!activationMetadata.ImplementsRegistryContract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
registry = null;
return false;
}
if (activationMetadata.IsAbstract)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
registry = null;
return false;
}
if (activationMetadata.Factory is null)
{
logger.Warn(
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor.");
registry = null;
return false;
}
registry = activationMetadata.Factory();
return true;
}
/// <summary>
/// 调用所有已激活的 generated registry 完成 CQRS handler 注册,并保留稳定的调试日志顺序。
/// </summary>
/// <param name="services">目标服务集合。</param>
/// <param name="registries">已通过契约校验的 registry 实例。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
private static void RegisterGeneratedRegistries(
IServiceCollection services,
IReadOnlyList<ICqrsHandlerRegistry> registries,
string assemblyName,
ILogger logger)
{
foreach (var registry in registries)
{
logger.Debug(
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
registry.Register(services, logger);
}
}
/// <summary>
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
/// </summary>
/// <param name="reflectionFallbackMetadata">生成注册器声明的反射补扫元数据。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
/// <returns>描述 generated registry 是否已完全处理当前程序集的结果对象。</returns>
private static GeneratedRegistrationResult BuildGeneratedRegistrationResult(
ReflectionFallbackMetadata? reflectionFallbackMetadata,
string assemblyName,
ILogger logger)
{
if (reflectionFallbackMetadata is null)
return GeneratedRegistrationResult.FullyHandled();
if (reflectionFallbackMetadata.HasExplicitTypes)
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s).");
}
else
{
logger.Debug(
$"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers.");
}
return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata);
}
/// <summary>
/// 获取指定实现类型上所有受支持的 CQRS handler 接口,并缓存筛选与排序结果。
/// </summary>
@ -255,6 +333,29 @@ internal static class CqrsHandlerRegistrar
return null;
var resolvedTypes = new List<Type>();
AppendDirectFallbackTypes(fallbackAttributes, resolvedTypes, assemblyName, logger);
AppendNamedFallbackTypes(assembly, fallbackAttributes, resolvedTypes, assemblyName, logger);
return new ReflectionFallbackMetadata(
resolvedTypes
.Distinct()
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToArray());
}
/// <summary>
/// 追加 attribute 里直接携带的 fallback 类型,并过滤掉跨程序集误声明的条目。
/// </summary>
/// <param name="fallbackAttributes">当前程序集上的 fallback attribute 集合。</param>
/// <param name="resolvedTypes">待补充的已解析类型集合。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
private static void AppendDirectFallbackTypes(
IReadOnlyList<CqrsReflectionFallbackAttribute> fallbackAttributes,
ICollection<Type> resolvedTypes,
string assemblyName,
ILogger logger)
{
foreach (var fallbackType in fallbackAttributes
.SelectMany(static attribute => attribute.FallbackHandlerTypes)
.Where(static type => type is not null)
@ -273,37 +374,65 @@ internal static class CqrsHandlerRegistrar
resolvedTypes.Add(fallbackType);
}
}
/// <summary>
/// 追加 attribute 里以类型名声明的 fallback 条目,并保留逐项失败的诊断能力。
/// </summary>
/// <param name="assembly">当前待解析的程序集。</param>
/// <param name="fallbackAttributes">当前程序集上的 fallback attribute 集合。</param>
/// <param name="resolvedTypes">待补充的已解析类型集合。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="logger">日志记录器。</param>
private static void AppendNamedFallbackTypes(
Assembly assembly,
IReadOnlyList<CqrsReflectionFallbackAttribute> fallbackAttributes,
ICollection<Type> resolvedTypes,
string assemblyName,
ILogger logger)
{
foreach (var typeName in fallbackAttributes
.SelectMany(static attribute => attribute.FallbackHandlerTypeNames)
.Where(static name => !string.IsNullOrWhiteSpace(name))
.Distinct(StringComparer.Ordinal)
.OrderBy(static name => name, StringComparer.Ordinal))
{
try
{
var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false);
if (type is null)
{
logger.Warn(
$"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry.");
continue;
}
TryAppendNamedFallbackType(assembly, resolvedTypes, assemblyName, typeName, logger);
}
}
resolvedTypes.Add(type);
}
catch (Exception exception)
/// <summary>
/// 解析并追加单个按名称声明的 fallback 类型,同时保留“找不到”与“加载异常”两类不同日志语义。
/// </summary>
/// <param name="assembly">当前待解析的程序集。</param>
/// <param name="resolvedTypes">待补充的已解析类型集合。</param>
/// <param name="assemblyName">当前程序集的稳定名称。</param>
/// <param name="typeName">要解析的完整类型名。</param>
/// <param name="logger">日志记录器。</param>
private static void TryAppendNamedFallbackType(
Assembly assembly,
ICollection<Type> resolvedTypes,
string assemblyName,
string typeName,
ILogger logger)
{
try
{
var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false);
if (type is null)
{
logger.Warn(
$"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}");
$"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry.");
return;
}
}
return new ReflectionFallbackMetadata(
resolvedTypes
.Distinct()
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
.ToArray());
resolvedTypes.Add(type);
}
catch (Exception exception)
{
logger.Warn(
$"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}");
}
}
/// <summary>

View File

@ -7,33 +7,62 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-001`
- 当前阶段:`Phase 1`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-012`
- 当前阶段:`Phase 12`
- 当前焦点:
- 已将旧 `local-plan/` 迁入 `ai-plan/public/analyzer-warning-reduction/`active 入口只保留当前恢复信息
- 基于现有剩余热点,评估 `MA0051``MA0048``MA0046` 与少量 `MA0016` 是否适合继续在同一主线上处理
- 若继续推进,优先选择不引入 API rename、公共契约漂移或 Godot 宿主不稳定测试的切入点
- 当前 PR review workflow 已补强到支持 JSON 落盘与按 section/path 收窄输出;下一轮恢复到 `MA0046` 主批次
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
- 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015``MA0077`
只是当前最明显的低数量示例,不构成限定
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
## 当前状态摘要
- 已完成 `GFramework.Core``GFramework.Cqrs``GFramework.Godot` 与部分 source generator 的低风险 warning 清理
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
- 当前剩余 warning 已集中到长方法、文件/类型命名冲突、delegate 形状和少量公共集合抽象接口问题
- 已完成当前 PR #265 review follow-up修复 `CoroutineScheduler` 的零容量扩容边界,并补上 `Store` dispatch 作用域的异常安全回滚
- 已继续完成当前 PR #265 review follow-up修复 `Event<T>``Event<T, TK>` 监听器计数的 off-by-one并补充回归测试
- 已增强 `gframework-pr-review` 脚本与 skill 文档,降低超长 JSON 直出导致的 review 信号漏看风险
- 当前 `PauseStackManager``Store``CoroutineScheduler``GFramework.Core``MA0048`
文件/类型命名冲突已从 active 入口移除;主题内剩余 warning 主要集中在 `MA0046` delegate 形状、
`MA0016` 集合抽象接口、`MA0002` comparer 重载,以及 `MA0015` / `MA0077` 两个低数量尾项
## 当前活跃事实
- 当前主题仍是 active topic因为剩余结构性 warning 是否继续推进尚未决策
- `RP-001` 的详细实现历史、测试记录和 warning 热点清单已归档到主题内 `archive/`
- `RP-002` 已在不改公共契约的前提下完成 `CqrsHandlerRegistrar` 结构拆分,并通过定向 build/test 验证
- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证
- `RP-004` 已完成当前 PR review follow-up修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题
- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试
- `RP-006` 已在不改公共 API 的前提下完成 `Store` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证 dispatch、
多态 reducer 匹配与历史语义未回归
- `RP-007` 已在不改公共 API 的前提下完成 `CoroutineScheduler` 两个 `MA0051` 的结构拆分,并通过定向 build/test 验证
调度、取消与完成状态语义未回归
- `RP-008` 将后续策略从“单文件 warning 切片”切换为“按类型批处理 + 文件数上限控制”,并允许在非冲突前提下使用
不同模型的 subagent 并行处理
- `RP-009` 在不改公共 API 的前提下,将同名泛型家族收拢到与类型名一致的单文件中,清空当前 `GFramework.Core`
`net8.0` 基线中的 `MA0048`,并通过定向 build/test 验证 `Command``Query``Event` 路径未回归
- `RP-010` 使用 `gframework-pr-review` 复核当前分支 PR #265 后,修复了仍在本地成立的两个 follow-up 风险:
`CoroutineScheduler``initialCapacity: 0` 扩容越界,以及 `Store` 在 dispatch 快照阶段抛异常时可能残留
`_isDispatching = true` 的锁死问题
- `RP-011` 根据补充复核继续收口 PR #265 的 outside-diff comment修复 `Event<T>` / `Event<T, TK>` 默认 no-op
委托导致的 `GetListenerCount()` off-by-one并以定向事件测试验证注册、注销和计数语义
- `RP-012``gframework-pr-review` 增加 `--json-output``--section``--path` 与文本截断能力,并更新 skill 推荐用法,
让“先落盘、再定向抽取”成为默认可操作路径
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
- 结构性重构风险:剩余 `MA0051``MA0048` 可能要求较大的文件拆分或类型重命名
- 缓解措施:只在下一轮明确接受结构调整成本时再继续推进,不在恢复点模糊的情况下顺手扩面
- 公共契约兼容风险:剩余 `MA0046` / `MA0016` 若直接改公开委托或集合类型,可能波及用户代码
- 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
- 缓解措施:只在 warning 类型或目录边界清晰时并行;每个 subagent 必须有独占文件 ownership主代理负责合并验证
## 活跃文档
@ -43,10 +72,60 @@
## 验证说明
- `RP-001` 的详细 warning 清理、回归修复与定向验证命令均已迁入主题内历史归档
- `RP-002` 的定向验证结果:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=`
- `RP-003` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:Summary;WarningsOnly`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=`
- `RP-004` 的定向验证结果:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- 结果:`0 Warning(s)``0 Error(s)`
- `RP-005` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`27 Warning(s)``0 Error(s)``PauseStackManager.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
- 结果:`25 Passed``0 Failed`
- `RP-006` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`25 Warning(s)``0 Error(s)``Store.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~StoreTests -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`30 Passed``0 Failed`
- `RP-007` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)``CoroutineScheduler.cs` 已不再出现在 `MA0051` 列表中
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~CoroutineScheduler -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`34 Passed``0 Failed`
- `RP-008` 的策略基线:
- 当前 `GFramework.Core` 剩余 warning 分布:`MA0048=8``MA0046=6``MA0016=5``MA0002=2``MA0015=1``MA0077=1`
- 后续批处理规则:优先按类型推进;若当轮主类型数量不足,可顺手吸收其他低冲突类型,不限定于 `MA0015``MA0077`
- `RP-009` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`15 Warning(s)``0 Error(s)`;当前 `GFramework.Core` `net8.0` warnings-only 输出中已不再出现 `MA0048`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AbstractAsyncCommandTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AbstractAsyncQueryTests|FullyQualifiedName~EventTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`83 Passed``0 Failed`
- `RP-010` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`;新增修复未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CoroutineSchedulerTests.Run_Should_Grow_From_Zero_Initial_Capacity|FullyQualifiedName~StoreTests.Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- `RP-011` 的定向验证结果:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)``Event.cs` 的 listener count 修复未引入新的 `GFramework.Core` `net8.0` 构建错误
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~EventTests.EventT_GetListenerCount_Should_Exclude_Placeholder_Handler|FullyQualifiedName~EventTests.EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- `RP-012` 的定向验证结果:
- `python3 -m py_compile .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
- 结果:通过;使用 `PYTHONPYCACHEPREFIX=/tmp/codex-pycache` 规避技能目录只读导致的 `__pycache__` 写入限制
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --help`
- 结果:通过;`--json-output``--section``--path``--max-description-length` 已出现在 CLI 帮助中
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking再按需展开历史归档中的 warning 热点与验证记录
2. 从 `MA0051``MA0048``MA0046` 中只选一个结构性切入点继续,不要在同一轮同时扩多个风险面
3. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
2. 下一轮优先以 `MA0046` 为主批次启动,先从 `Architecture*``CoroutineScheduler` 的低风险 delegate 形状修正中选一个切入点
3. 若 `MA0046` 的文件 ownership 可以清晰切分,允许使用不同模型的 subagent 并行处理互不冲突的目录或类型簇
4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`

View File

@ -1,5 +1,275 @@
# Analyzer Warning Reduction 追踪
## 2026-04-21 — RP-012
### 阶段PR review workflow 输出收窄增强RP-012
- 背景:上一轮虽然脚本已经能解析 `outside_diff_comments`,但直接把超长 JSON 打到终端时仍可能因为输出截断而漏看高价值 review 信号
- 本轮对 `gframework-pr-review` 做了工作流级增强,而不是继续依赖 shell 重定向技巧:
- 为 `fetch_current_pr_review.py` 增加 `--json-output <path>`,允许把完整 JSON 稳定写入文件
- 增加 `--section`,可只输出 `outside-diff``open-threads``megalinter` 等高信号文本摘要
- 增加 `--path`,允许把文本输出收窄到特定文件或路径片段
- 增加 `--max-description-length`,避免超长 comment/body 在 text 模式下刷屏
- 当 text 模式搭配 `--json-output`stdout 保持精简,并显式提示完整 JSON 文件路径
- 同步更新 `SKILL.md`
- 将“先落盘,再用 `jq``--section` / `--path` 缩小范围”写成推荐机器工作流
- 补充按 section 和按路径聚焦的示例命令
- 预期收益:
- 不再要求操作者肉眼阅读整份长 JSON
- outside-diff、nitpick 和 open thread 都能成为一等可过滤输出
- 即使终端输出有 token/长度上限,完整结果仍可通过文件稳定回查
- 定向验证命令:
- `python3 -m py_compile .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
- 结果:通过;使用 `PYTHONPYCACHEPREFIX=/tmp/codex-pycache` 规避 `__pycache__` 写入限制
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --help`
- 结果:通过;新增 CLI 选项均已出现在帮助输出中
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`0 Warning(s)``0 Error(s)`
- 下一步建议:
- 之后执行 `$gframework-pr-review` 时,默认优先使用 `--json-output`
- 在 review 跟进阶段,先看 `outside-diff``open-threads``megalinter` 三个 section再决定是否需要打开完整 JSON
## 2026-04-21 — RP-011
### 阶段PR #265 outside-diff follow-up 补收口RP-011
- 用户补充指出 CodeRabbit 在 `Some comments are outside the diff` 中还有 `GFramework.Core/Events/Event.cs` 的 minor finding
默认 no-op 委托会被 `GetInvocationList()` 计入,导致 `GetListenerCount()` 在无监听器和单监听器场景分别返回 `1``2`
- 本地复核确认该问题仍成立:
- `Event<T>` 当前字段初始化为 `_ => { }`
- `Event<T, TK>` 当前字段初始化为 `(_, _) => { }`
- 两个 `Trigger(...)` 实现本身已是 null-safe因此无需依赖占位委托规避空引用
- 实施最小修复:
- 移除两个事件字段的 no-op 初始委托,改为以 `null` 表示“无监听器”
- 保持 `Register` / `UnRegister` / `Trigger` 的公开 API 和调用方式不变
- 在 `EventTests` 中新增单参数与双参数 `GetListenerCount()` 回归测试,覆盖初始值、注册后和注销后的计数语义
- 过程说明:
- 这条不是 skill 设计遗漏;`gframework-pr-review` 的目标本来就包含 latest review body 和 outside-diff 信号
- 上一轮是我在处理时漏看了这条 outside-diff item且终端里展示的超长 JSON 输出被截断,未单独把 `Event.cs` 项再抽出来复核
- 定向验证命令:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~EventTests.EventT_GetListenerCount_Should_Exclude_Placeholder_Handler|FullyQualifiedName~EventTests.EventTTK_GetListenerCount_Should_Exclude_Placeholder_Handler" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- 下一步建议:
- 若继续 PR #265 follow-up只接受当前本地仍成立的剩余 outside-diff 或 unresolved review 项
- 若没有新的有效 review 点,再恢复到 `MA0046` 主批次
## 2026-04-21 — RP-010
### 阶段PR #265 follow-up 收口RP-010
- 使用 `gframework-pr-review` 抓取当前分支 PR #265 的 latest head review threads、CodeRabbit review body、MegaLinter 摘要与 CTRF
测试结果;确认最新 unresolved thread 只剩 `CoroutineScheduler` 零容量扩容边界
- 本地复核后确认两处仍成立的风险:
- `CoroutineScheduler.Expand()``_slots.Length == 0` 时会把容量从 `0` 扩到 `0`,首次 `Run` 写槽位会越界
- `Store.EnterDispatchScope()``_isDispatching = true` 之后、快照构建完成之前若抛异常,会留下永久的嵌套分发误判
- 实施最小修复:
- 将 `Expand()` 调整为 `Math.Max(1, _slots.Length * 2)`,保持已有倍增策略,只补上零容量边界
- 为 `EnterDispatchScope()` 增加快照阶段的异常回滚,确保 `_isDispatching` 与实际 dispatch 生命周期保持一致
- 新增回归测试覆盖零容量启动路径,以及 dispatch 快照阶段抛错后的可恢复性
- 当前 PR 信号复核结论:
- CTRF最新评论显示 `2135 passed / 0 failed`
- MegaLinter唯一告警仍是 CI 中 `dotnet-format` restore 失败,未发现新的本地代码格式问题
- 旧 review body 中提到的 `Store` 异常安全问题虽未表现为最新 open thread但在本地代码中仍可成立因此一并收口
- 定向验证命令:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo`
- 结果:`15 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CoroutineSchedulerTests.Run_Should_Grow_From_Zero_Initial_Capacity|FullyQualifiedName~StoreTests.Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`2 Passed``0 Failed`
- 下一步建议:
- 若继续本主题,恢复到 `MA0046` 主批次,不再停留在当前 PR follow-up
- 若 PR review 还出现新线程,继续遵守“只修复当前本地仍成立的问题”的策略
## 2026-04-21 — RP-009
### 阶段:`MA0048` 批次收口RP-009
- 依据 `RP-008` 的批处理策略,本轮继续从 `GFramework.Core``MA0048` 启动,但不采用重命名公共类型的高风险做法;
改为把同名不同泛型 arity 的家族收拢到与类型名一致的单文件中
- 具体调整:
- 将 `AbstractCommand<TInput>``AbstractCommand<TInput, TResult>` 合并进 `AbstractCommand.cs`
- 将 `AbstractAsyncCommand<TInput>``AbstractAsyncCommand<TInput, TResult>` 合并进 `AbstractAsyncCommand.cs`
- 将 `AbstractQuery<TInput, TResult>` 合并进 `AbstractQuery.cs`
- 将 `AbstractAsyncQuery<TInput, TResult>` 合并进 `AbstractAsyncQuery.cs`
- 将泛型 `Event<T>` / `Event<T, TK>``EasyEventGeneric.cs` 迁移到 `Event.cs`
- 首次构建暴露出合并后的 `ICommand<TResult>` / `IQuery<TResult>` 命名空间歧义;随后改用
`GFramework.Core.Abstractions.*` 的限定名完成最小修正,没有引入行为改动
- 定向验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo -clp:"Summary;WarningsOnly"`
- 结果:`15 Warning(s)``0 Error(s)``MA0048` 已从当前 `GFramework.Core` `net8.0` warnings-only 基线中清空
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AbstractAsyncCommandTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AbstractAsyncQueryTests|FullyQualifiedName~EventTests" -m:1 -p:RestoreFallbackFolders="" -nologo`
- 结果:`83 Passed``0 Failed`
- 当前建议的下一批次顺序更新为:
- 第一优先级:`MA0046`
- 第二优先级:`MA0016`
- 顺手吸收:`MA0015``MA0077`
- 单独评估:`MA0002`
## 2026-04-21 — RP-008
### 阶段批处理策略切换RP-008
- 根据当前 `GFramework.Core` warnings-only build 的剩余分布,后续不再默认沿用“单文件、单 warning family”的切片节奏
改为按 warning 类型和数量优先级批量推进
- 当前数量基线:
- `MA0048 = 8`
- `MA0046 = 6`
- `MA0016 = 5`
- `MA0002 = 2`
- `MA0015 = 1`
- `MA0077 = 1`
- 新的批处理规则:
- 先按类型选择主批次,而不是按单文件选切入点
- 若主批次数量不够,则允许顺手并入其他低冲突类型;`MA0015``MA0077` 只是当前明显的低数量尾项示例,不是限定范围
- 单次 `boot` 的工作树改动规模控制在约 `100` 个文件以内,避免 recovery context 和 review 面同时膨胀
- 当 warning 类型或目录边界清晰且写集不冲突时,允许使用不同模型的 subagent 并行处理,但必须先定义独占 ownership
- 当前建议的下一批次顺序:
- 第一优先级:`MA0048`
- 第二优先级:`MA0046`
- 顺手吸收:其他低冲突类型,当前可见示例包括 `MA0015``MA0077`
- 单独评估:`MA0016``MA0002`
- 本轮仅更新 recovery strategy不改生产代码验证继续沿用当前基线构建
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)`
## 2026-04-21 — RP-007
### 阶段CoroutineScheduler `MA0051` 收口RP-007
- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
`GFramework.Core/Coroutine/CoroutineScheduler.cs`,因为剩余两个 `MA0051` 都集中在协程启动与完成清理路径,且已有
`CoroutineSchedulerTests``CoroutineSchedulerAdvancedTests` 覆盖句柄创建、取消、完成状态、标签分组和等待语义
- 将 `Run` 拆分为:
- `AllocateSlotIndex`
- `CreateRunningSlot`
- `RegisterCancellationCallback`
- `RegisterStartedCoroutine`
- `CreateCoroutineMetadata`
- `ResetCompletionTracking`
- 将 `FinalizeCoroutine` 拆分为:
- `TryGetFinalizableCoroutine`
- `UpdateCompletionMetadata`
- `ApplyCompletionMetadata`
- `ReleaseCompletedCoroutine`
- `CompleteCoroutineLifecycle`
- 保持取消回调只做跨线程入队、`Prewarm` 时机、统计记录文本、`RemoveTag` / `RemoveGroup` / `WakeWaiters` 顺序以及
`OnCoroutineFinished` 的同步触发时机不变,只收缩主方法长度并补齐辅助方法意图注释
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`23 Warning(s)``0 Error(s)``CoroutineScheduler.cs` 已不再出现在 `MA0051` 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~CoroutineScheduler -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`34 Passed``0 Failed`
- 当前 `MA0051` 主线已经在本主题下完成;下一步若继续,应先重新评估剩余 `MA0048``MA0046``MA0002``MA0016`
收敛价值与改动风险,再决定是否开启下一轮 warning family
## 2026-04-21 — RP-006
### 阶段Store `MA0051` 收口RP-006
- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
`GFramework.Core/StateManagement/Store.cs`,因为该文件的两个 `MA0051` 都集中在 dispatch / reducer snapshot 逻辑,
且已有 `StoreTests` 覆盖 dispatch、batch、history 和多态 reducer 匹配语义
- 在正式验证前先处理 WSL 环境噪音:当前 worktree 的 `GFramework.Core/obj/project.assets.json` 是 Windows 侧 restore
产物,`--no-restore` 构建会继续引用宿主 Windows fallback package folder本轮先执行一次 Linux 侧
`dotnet restore GFramework.Core/GFramework.Core.csproj -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> --ignore-failed-sources -nologo`
刷新资产文件,再继续 warnings-only build
- 将 `Dispatch` 拆分为:
- `EnterDispatchScope`
- `TryCommitDispatchResult`
- `ExitDispatchScope`
- 将 `CreateReducerSnapshotCore` 拆分为:
- `CreateExactReducerSnapshot`
- `CreateAssignableReducerSnapshot`
- `CollectReducerMatches`
- `CompareReducerMatch`
- 保持 `_dispatchGate -> _lock` 的锁顺序、middleware 锁外执行、批处理通知折叠以及“精确类型 -> 基类 -> 接口 ->
注册顺序”的 reducer 稳定排序语义不变,只收缩主方法长度并补齐辅助方法意图注释
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo -clp:"Summary;WarningsOnly"`
- 结果:`25 Warning(s)``0 Error(s)``Store.cs` 已不再出现在 `MA0051` 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~StoreTests -p:RestoreFallbackFolders="" -p:RestorePackagesPath=<linux-nuget-cache> -nologo`
- 结果:`30 Passed``0 Failed`
- 下一步保持同一节奏:只在 `CoroutineScheduler.cs``Run` / `FinalizeCoroutine` 两个 `MA0051` 中继续,不与其他
warning 家族混做
## 2026-04-21 — RP-005
### 阶段PauseStackManager `MA0051` 收口RP-005
- 按 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
`GFramework.Core/Pause/PauseStackManager.cs`,因为该文件体量明显小于 `CoroutineScheduler``Store`
且已有稳定的 `PauseStackManagerTests` 覆盖暂停栈、跨组独立性、事件通知与并发 `Push/Pop` 行为
- 先用 `warnings-only` 定向构建确认 `DestroyAsync``Pop` 仍分别命中 `MA0051`,再把逻辑拆分为:
- `TryBeginDestroy`
- `NotifyDestroyedGroups`
- `TryPopEntry`
- `RemoveEntryFromStack`
- 额外抽出 `CreateHandlerSnapshot``NotifyHandlersSnapshot`,统一普通通知与销毁补发路径的处理器排序和异常日志,
保持原有“锁内采集快照、锁外调用处理器与事件”的并发策略不变
- 为销毁路径新增 `DestroyAsync_Should_NotifyResumedGroups`,验证当多个暂停组在销毁前仍为暂停态时,
处理器和事件订阅者都会收到 `IsPaused=false` 的恢复信号
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
- 结果:`27 Warning(s)``0 Error(s)``PauseStackManager.cs` 已不再出现在 `MA0051` 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
- 结果:`25 Passed``0 Failed`
- 下一步保持原节奏:只在 `CoroutineScheduler``Store` 中二选一继续,不与其他 warning 家族混做
## 2026-04-21 — RP-003
### 阶段Architecture 生命周期 `MA0051` 收口RP-003
- 依据 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,选定
`GFramework.Core/Architectures/ArchitectureLifecycle.cs`,因为文件体量适中且已有
`ArchitectureLifecycleBehaviorTests` 覆盖阶段流转、销毁顺序和 late registration 行为
- 先用 `warnings-only` 定向构建确认 `ArchitectureLifecycle.InitializeAllComponentsAsync` 仍在报
`MA0051`,随后把主流程拆成:
- `CreateInitializationPlan`
- `InitializePhaseComponentsAsync`
- `MarkInitializationCompleted`
- 保持原有阶段顺序 `Before* -> After*`、批量日志文本和异步初始化策略不变,只压缩主方法长度
- 修正新增 `InitializationPlan` 记录类型的 XML `<param>` 名称大小写,避免引入文档告警
- 验证通过:
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:Summary;WarningsOnly`
- 结果:`29 Warning(s)``0 Error(s)``ArchitectureLifecycle.cs` 已不再出现在 warning 列表
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~ArchitectureLifecycleBehaviorTests -p:RestoreFallbackFolders=`
- 结果:`6 Passed``0 Failed`
## 2026-04-21 — RP-004
### 阶段PR review follow-upRP-004
- 使用 `gframework-pr-review` 抓取当前分支 PR #263 的最新 CodeRabbit review threads、MegaLinter 摘要与 CTRF 测试结果,
只接受仍能在本地工作树复现的 review 点
- 在 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs` 中将 `TryCreateGeneratedRegistry``out` 参数改为
`[NotNullWhen(true)] out ICqrsHandlerRegistry?`,移除三处 `null!` 抑制,保持激活失败时的日志文本与回退语义不变
- 修正 active trace 中重复的 `## 2026-04-21` 二级标题,消除 CodeRabbit 报告的 markdownlint `MD024`
- 核实 PR 信号后确认:当前 CTRF 报告为 `2134 passed / 0 failed`MegaLinter 唯一告警来自 CI 环境中的 `dotnet-format`
restore 失败,不是本地代码格式问题
- 验证通过:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- 结果:`0 Warning(s)``0 Error(s)`
## 2026-04-21 — RP-002
### 阶段CQRS `MA0051` 收口RP-002
- 依据 active tracking 中“先只选一个结构性切入点”的约束,选定 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`
作为低风险下一步,因为它已有稳定的 targeted test 覆盖 generated registry、reflection fallback、缓存和重复注册行为
- 将 `TryRegisterGeneratedHandlers` 拆分为 registry 激活、批量注册和 fallback 结果构建三个辅助阶段,同时把
`GetReflectionFallbackMetadata` 的直接类型解析与按名称解析拆开,降低长方法复杂度但不改日志文本与回退语义
- 顺手修正 `RegisterAssemblyHandlers` 内部调试日志的缩进,未改注册顺序、生命周期或服务描述符写入逻辑
- 验证通过:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- 结果:`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter FullyQualifiedName~CqrsHandlerRegistrarTests -p:RestoreFallbackFolders=`
- 结果:`11 Passed``0 Failed`
- 新发现的环境注意事项:
- 当前 WSL worktree 下若不显式传入 `-p:RestoreFallbackFolders=`Linux `dotnet` 会读取不存在的 Windows fallback package
folder 并导致 `ResolvePackageAssets` 失败
- sandbox 内运行 `dotnet` 会因 MSBuild named-pipe 限制失败;需要在提权上下文中执行 .NET 验证
## 2026-04-19
### 阶段local-plan 迁移收口RP-001
@ -28,5 +298,5 @@
### 下一步
1. 后续若继续 analyzer warning reduction只从 `ai-plan/public/analyzer-warning-reduction/` 进入,不再恢复 `local-plan/`
2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/`
1. 若继续 analyzer warning reduction优先回到 `GFramework.Core` 剩余 `MA0051` 热点,并继续保持“单 warning family、单切入点”的节奏
2. 后续所有 WSL 下的 .NET 定向验证命令继续显式附带 `-p:RestoreFallbackFolders=`,避免把环境问题误判成代码回归

View File

@ -7,21 +7,21 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-003`
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-007`
- 当前阶段:`Phase 3`
- 当前焦点:
- 已完成 `docs/zh-CN/core/architecture.md`、`context.md``lifecycle.md``command.md``query.md`
`cqrs.md` 的专题页重写
- `core` 关键专题页已改回当前 `Architecture``ArchitectureContext`、旧 Command/Query 兼容层与新 CQRS
runtime 的真实入口语义
- 下一轮需要继续推进 `docs/zh-CN/core/*` 余下专题页,以及 `docs/zh-CN/game/*`
`docs/zh-CN/source-generators/*` 的专题页核对
- 已完成 `docs/zh-CN/core/events.md`、`property.md``logging.md` 的专题页重写
- 已按源码与测试复核 `docs/zh-CN/core/state-management.md``coroutine.md`,当前内容与实现基本一致,无需再做
机械改写
- 已完成 `docs/zh-CN/game/scene.md``ui.md` 的专题页重写,当前内容已回到“项目自接 factory/root + router 基类”的真实边界
- 已完成 `docs/zh-CN/source-generators/context-aware-generator.md``priority-generator.md` 的专题页重写,当前内容已回到“真实生成成员、推荐 API 与兼容边界”的结构
- 下一轮需要把重心转到 Godot 相关生成器页面核对
## 当前状态摘要
- 文档治理规则已收口到仓库规范README、站点入口与采用链路不再依赖旧文档自证
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态
- 当前主题仍是 active topic因为 `core` 其余专题页及 `game``source-generators` 栏目下仍可能包含与实现漂移的旧内容
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态,本轮计划中的 `core` 剩余高风险页面已完成收口
- 当前主题仍是 active topic因为 `source-generators` 栏目下的 Godot 相关页面仍可能包含与实现漂移的旧内容
## 当前活跃事实
@ -32,16 +32,38 @@
- `docs` 站点构建已验证通过,修正了 VitePress 对 `docs/` 目录外相对链接的 dead-link 检查问题
- `core` 关键专题页已移除 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` 赋值式示例和已移除的
`RegisterMediatorBehavior` 等过时说明
- `core/index.md` 已把 `Godot``Source Generators` 栏目入口改成可点击链接,补齐 landing page 导航一致性
- `documentation-governance-and-refresh` active trace 已把重复的 `### 下一步` 标题改成带恢复点标识的唯一标题,消除
`MD024/no-duplicate-heading` 告警
- `gframework-pr-review` 脚本已修复“空 `APPROVED` review 覆盖非空 CodeRabbit review body”的解析路径当前分支可重新提取 Nitpick comments
- `docs/zh-CN/core/events.md``property.md``logging.md` 已改成“当前角色、最常用入口、边界和迁移建议”的结构,
不再复刻旧版大而全 API 列表
- `docs/zh-CN/core/property.md` 已明确记录 `BindableProperty<T>.Comparer` 的闭合泛型级共享语义,避免文档继续误导读者把
`WithComparer(...)` 当成实例级配置
- `docs/zh-CN/core/state-management.md``coroutine.md` 已按当前 runtime / 测试重新核对,当前内容可继续保留
- `docs/zh-CN/game/scene.md` 已改成“真实公开入口、场景栈语义、factory/root 装配、过渡处理器与守卫扩展点”的结构,
不再暗示框架自带统一场景注册与完整引擎装配
- `docs/zh-CN/game/ui.md` 已改成“Page 栈、layer UI、输入动作仲裁、World 阻断与暂停语义”的结构,明确 `Show(...)`
不适用于 `UiLayer.Page`
- 本轮重写后再次执行 `cd docs && bun run build` 通过,当前 `game` 栏目入口与专题页改动没有破坏站点构建
- `docs/zh-CN/source-generators/context-aware-generator.md` 已改成“真实生成成员、provider/实例缓存语义、与 `ContextAwareBase` 的边界、测试接法”的结构,
不再用旧版简化生成代码替代当前实现
- `docs/zh-CN/source-generators/priority-generator.md` 已改成“生成 `IPrioritized`、priority-aware 检索 API、动态优先级边界与诊断”的结构
不再把 `GetAllByPriority<T>()` / `system.Init()` 当作所有场景的默认示例
- 本轮重写后再次执行 `cd docs && bun run build` 通过,当前 `source-generators` 栏目改动没有破坏站点构建
## 当前风险
- 旧专题页示例失真风险:`docs/zh-CN/core/*``game/*``source-generators/*` 中仍可能保留看似合理但与
真实实现不一致的示例
- 缓解措施:继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对,不把旧文档当事实来源
- 旧专题页示例失真风险:`docs/zh-CN/game/*` 与 `source-generators/*` 中仍可能保留看似合理但与真实实现不一致的示例
- 缓解措施:`game/scene.md``ui.md``source-generators/context-aware-generator.md``priority-generator.md` 已完成收口;
继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对剩余 Godot 相关页面,不把旧文档当事实来源
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
- Active 入口回膨胀风险:后续若把栏目级重写过程直接追加到 active 文档,会再次拖慢恢复
- 缓解措施:阶段完成并验证后,继续把细节迁入本 topic 的 `archive/`
- review 跟进遗漏风险:如果 PR review 抓取继续优先选中空 review body会漏掉 CodeRabbit 的 Nitpick 和
linter 跟进项
- 缓解措施:保持当前“最新提交 + 最新非空 CodeRabbit review body”解析策略并在有疑点时以 API 实抓结果复核
## 活跃文档
@ -53,10 +75,11 @@
- 旧 `local-plan/` 的详细实施历史与文档站构建结果已迁入主题内归档
- active 跟踪文件已按 `ai-plan` 治理规则精简为当前恢复入口
- `cd docs && bun run build`
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
## 下一步
1. 继续核对 `docs/zh-CN/core/*` 余下专题页,优先处理 `events``property``state-management``coroutine`
`logging`
2. 再推进 `docs/zh-CN/game/*``docs/zh-CN/source-generators/*` 的专题页重写,优先处理仍引用旧安装方式或旧 API 的页面
3. 若专题页批量重写完成且验证通过,将本轮 `core` 专题页收口和后续修订过程迁入本 topic 的 `archive/`
1. 继续核对 Godot 相关生成器页面,优先处理 `godot-project-generator.md``get-node-generator.md`
`bind-node-signal-generator.md`
2. 重点确认 `project.godot``AutoLoad` / `InputActions``GetNode` / `BindNodeSignal` 示例仍与当前包关系和生成器入口一致
3. 若 active trace 再积累新的已完成阶段,按恢复点粒度迁入 `archive/traces/`,避免默认启动入口再次膨胀

View File

@ -25,7 +25,7 @@
- 历史 trace 归档:
- `ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-history-through-2026-04-18.md`
### 下一步
### 下一步RP-001
1. 后续继续该主题时,只从 `ai-plan/public/documentation-governance-and-refresh/` 进入,不再恢复 `local-plan/`
2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/`
@ -47,7 +47,7 @@
- 当前默认导航入口已显著收敛,但专题页仍需逐页按源码与测试继续核对
- 后续优先级应从 `core` 专题页开始,再向 `game``source-generators` 扩展
### 下一步
### 下一步RP-002
1. 审核 `docs/zh-CN/core/architecture.md``context.md``lifecycle.md``command.md``query.md``cqrs.md`
2. 记录每页的失真点、真实 API 名称与应保留的最小示例
@ -77,8 +77,107 @@
是公开入口
- 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页重写没有破坏文档站构建
### 下一步
### 下一步RP-003
### 补充2026-04-21 PR review 跟进收口RP-004
- 通过 `gframework-pr-review` 复查当前分支 PR 时发现:脚本把同一 head commit 上空 body 的 `APPROVED`
review 误当成“最新 review body”导致 `Nitpick comments` 未被结构化提取
- 对照 GitHub API 的 review 列表后,确认真正包含 `Nitpick comments (2)` 的是更早 3 秒提交的
`COMMENTED` review因此调整脚本为“保持最新 review 元数据输出不变,但解析时优先选择同一提交上的最新非空
CodeRabbit review body”
- 根据重新提取的 Nitpick 内容,补齐 `docs/zh-CN/core/index.md``Godot``Source Generators`
栏目的可点击链接
- 顺手修正 active trace 中重复的 `### 下一步` 标题,消除 `MD024/no-duplicate-heading` 告警,避免后续 PR
review 再次把文档治理入口本身标成噪音
### 验证RP-004
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
- `cd docs && bun run build`
### 下一步RP-004
1. 继续处理 `docs/zh-CN/core/events.md``property.md``state-management.md``coroutine.md``logging.md`
2. 保持同样的证据顺序:源码、`*.csproj`、模块 README、`ai-libs/` 参考实现
3. 完成下一批专题页重写后再次执行 `cd docs && bun run build`
2. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节
3. 保持 PR review 跟进时优先验证最新未解决线程、非空 CodeRabbit review body 与 MegaLinter 明确告警
### 阶段Core 剩余高风险专题页核对RP-005
- 依据 `documentation-governance-and-refresh` active tracking 的恢复点,继续核对
`docs/zh-CN/core/events.md``property.md``state-management.md``coroutine.md``logging.md`
- 对照 `GFramework.Core/Events/*``Property/*``Logging/*``StateManagement/*``Coroutine/*` 以及对应测试后确认:
- `events.md``property.md``logging.md` 仍带有旧版“大而全 API 列表”写法,与当前公开入口和推荐边界不匹配
- `state-management.md``coroutine.md` 已和当前 runtime / 测试语义基本对齐,本轮无需为了统一文风做额外重写
- 重写 `events.md`,使其回到“上下文入口、`EventBus` / `EnhancedEventBus`、优先级传播、局部事件对象、与 Store / CQRS
的边界”的当前结构
- 重写 `property.md`,使其回到“字段级响应式值、何时继续使用 `BindableProperty<T>`、何时切到 `Store<TState>`”的当前结构,
并补充 `BindableProperty<T>.Comparer` 按闭合泛型共享的兼容注意点
- 重写 `logging.md`,使其回到“`LoggerFactoryResolver` 默认行为、`ArchitectureConfiguration` 日志 provider 配置、
`IStructuredLogger` / `LogContext`、provider 替换边界”的当前结构
- 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页收口没有破坏文档站构建
### 当前结论RP-005
- 本轮计划中的 `core` 剩余高风险页面已完成核对;`state-management``coroutine` 经复核后可继续保留
- `core` 栏目下一步不再需要围绕这五页反复停留,后续重心应转到 `docs/zh-CN/game/*``docs/zh-CN/source-generators/*`
### 下一步RP-005
1. 继续核对 `docs/zh-CN/game/*`,优先处理仍引用旧安装方式、旧状态系统或旧 UI / Scene 接法的页面
2. 再推进 `docs/zh-CN/source-generators/*`,重点核对生成器 wiring、包关系与最小接入示例
3. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节
### 阶段Game Scene / UI 专题页收口RP-006
- 依据 `documentation-governance-and-refresh` active tracking 的下一步,优先复核 `docs/zh-CN/game/scene.md`
`docs/zh-CN/game/ui.md`
- 对照 `GFramework.Game.Abstractions/Scene/*``GFramework.Game.Abstractions/UI/*``GFramework.Game/Scene/SceneRouterBase.cs`
`GFramework.Game/UI/UiRouterBase.cs``GFramework.Game/README.md``ai-libs/CoreGrid` 参考接法后确认:
- `scene.md` 仍把场景系统写成框架自带完整注册/装配的一体化方案,没有突出 `ISceneFactory``ISceneRoot` 和项目侧
router 派生类的责任边界
- `ui.md` 仍按旧教程式结构展开,没有清楚区分 `Page` 栈与 `Overlay/Modal/Toast/Topmost` 层级 UI也缺少当前
`UiInteractionProfile``TryDispatchUiAction(...)` 与 World 输入阻断语义
- 重写 `scene.md`,使其回到“当前公开入口、场景栈语义、最小接入路径、守卫/过渡处理器扩展点、与旧写法的边界”的结构
- 重写 `ui.md`,使其回到“页面栈与层级 UI 分流、输入仲裁、暂停/阻断语义、最小接入路径、扩展点”的结构
- 新版两页都明确了factory、root、引擎节点与注册表仍由项目或适配层提供框架当前提供的是 router 基类与通用编排
### 验证RP-006
- `cd docs && bun run build`
### 下一步RP-006
1. 继续核对 `docs/zh-CN/source-generators/*`,优先处理仍引用旧初始化方式、旧聚合包名或过时 generator wiring 的页面
2. 重点复核 `priority-generator.md``context-aware-generator.md` 与 Godot 相关生成器页面,确认示例仍与当前 runtime /
generator 入口一致
3. 若 `source-generators` 出现多页连续收口结果,再按恢复点粒度整理 active trace避免默认入口继续膨胀
### 阶段Core Source Generator 关键专题页收口RP-007
- 依据 `documentation-governance-and-refresh` active tracking 的下一步,优先复核
`docs/zh-CN/source-generators/context-aware-generator.md``priority-generator.md`
- 对照 `GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs``GFramework.Core/Rule/ContextAwareBase.cs`
`GFramework.Core/Extensions/ContextAwareServiceExtensions.cs``GFramework.Core.SourceGenerators/Bases/PriorityGenerator.cs`
`GFramework.Core.Abstractions/Architectures/IArchitectureContext.cs` 与相关诊断定义后确认:
- `context-aware-generator.md` 仍在展示旧版简化生成代码,没有说明当前实例缓存、类型级共享 provider、同步锁以及
`ContextAwareBase` 的不同默认回退路径
- `priority-generator.md` 仍把 `[Priority]` 写成“标了就自动改变顺序”的教程式功能说明,并大量使用
`GetAllByPriority<T>()``system.Init()` 这类不适合作为当前 `IContextAware` 路径默认示例的旧写法
- 重写 `context-aware-generator.md`使其回到“最小用法、当前生成成员、provider 与实例缓存语义、与 `ContextAwareBase`
和 Context Get 注入的关系、测试边界”的结构
- 重写 `priority-generator.md`,使其回到“只生成 `IPrioritized`、priority-aware API 在不同层上的入口、动态优先级边界、
诊断与约束”的结构
- 新版两页都明确了:排序效果取决于调用方是否走 priority-aware API`[ContextAware]` 生成路径与
`ContextAwareBase` 不是同一套默认行为
### 验证RP-007
- `cd docs && bun run build`
### 下一步RP-007
1. 继续核对 Godot 相关生成器页面,优先处理 `godot-project-generator.md``get-node-generator.md`
`bind-node-signal-generator.md`
2. 重点确认 `project.godot``AutoLoad` / `InputActions``GetNode` / `BindNodeSignal` 示例仍与当前包关系和生成器入口一致
3. 若 Godot 页面也出现连续收口结果,再按恢复点粒度整理 active trace避免默认入口继续膨胀

View File

@ -1,602 +1,130 @@
# Events 包使用说明
# Events
## 概述
`GFramework.Core.Events` 是架构内的轻量广播层。它适合表达“某件事已经发生”的运行时信号、模块间松耦合通知,
以及为旧模块保留 `EventBus` 语义;如果你需要请求/响应、pipeline behavior 或 handler registry优先使用
[cqrs](./cqrs.md)。
Events 包提供了一套完整的事件系统实现了观察者模式Observer Pattern。通过事件系统可以实现组件间的松耦合通信支持无参和带参事件、事件注册/注销、以及灵活的事件组合。
## 安装方式
事件系统是 GFramework 架构中组件间通信的核心机制,与命令模式和查询模式共同构成了完整的 CQRS 架构。
## 核心接口
### IEvent
基础事件接口,定义了事件注册的基本功能。
**核心方法:**
```csharp
IUnRegister Register(Action onEvent); // 注册事件处理函数
```bash
dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions
```
### IUnRegister
事件实现位于 `GFramework.Core`,抽象接口位于 `GFramework.Core.Abstractions`
注销接口,用于取消事件注册。
## 最常用入口
**核心方法:**
如果你已经在 `ArchitectureContext` 或任何 `IContextAware` 对象里,最常见的入口仍然是:
- `SendEvent<TEvent>()`
- `SendEvent(eventData)`
- `RegisterEvent(Action<TEvent>)`
- `UnRegisterEvent(Action<TEvent>)`
示例:
```csharp
void UnRegister(); // 执行注销操作
```
using GFramework.Core.Extensions;
using GFramework.Core.System;
### IUnRegisterList
public sealed record PlayerDiedEvent(int PlayerId);
注销列表接口,用于批量管理注销对象。
**属性:**
```csharp
IList<IUnRegister> UnregisterList { get; } // 获取注销列表
```
### IEventBus
事件总线接口,提供基于类型的事件发送和注册。
**核心方法:**
```csharp
IUnRegister Register<T>(Action<T> onEvent); // 注册类型化事件
void Send<T>(T e); // 发送事件实例
void Send<T>() where T : new(); // 发送事件(自动创建实例)
void UnRegister<T>(Action<T> onEvent); // 注销事件监听器
```
## 核心类
### EasyEvent
无参事件类,支持注册、注销和触发无参事件。
**核心方法:**
```csharp
IUnRegister Register(Action onEvent); // 注册事件监听器
void Trigger(); // 触发事件
```
**使用示例:**
```csharp
// 创建事件
var onClicked = new EasyEvent();
// 注册监听
var unregister = onClicked.Register(() =>
public sealed class CombatSystem : AbstractSystem
{
Console.WriteLine("Button clicked!");
});
protected override void OnInit()
{
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
}
// 触发事件
onClicked.Trigger();
private void OnPlayerDied(PlayerDiedEvent @event)
{
Logger.Info("Player died: {0}", @event.PlayerId);
}
// 取消注册
unregister.UnRegister();
public void KillPlayer(int playerId)
{
this.SendEvent(new PlayerDiedEvent(playerId));
}
}
```
### Event`<T>`
如果你在架构外单独使用,也可以直接构造 `EventBus`
单参数泛型事件类,支持一个参数的事件。
## EventBus 与 EnhancedEventBus
**核心方法:**
默认实现是 `EventBus`,提供类型化发送与订阅:
```csharp
IUnRegister Register(Action<T> onEvent); // 注册事件监听器
void Trigger(T eventData); // 触发事件并传递参数
```
using GFramework.Core.Events;
**使用示例:**
```csharp
// 创建带参数的事件
var onScoreChanged = new Event<int>();
// 注册监听
onScoreChanged.Register(newScore =>
{
Console.WriteLine($"Score changed to: {newScore}");
});
// 触发事件并传递参数
onScoreChanged.Trigger(100);
```
### Event<T, TK>
双参数泛型事件类。
**核心方法:**
```csharp
IUnRegister Register(Action<T, TK> onEvent); // 注册事件监听器
void Trigger(T param1, TK param2); // 触发事件并传递两个参数
```
**使用示例:**
```csharp
// 伤害事件:攻击者、伤害值
var onDamageDealt = new Event<string, int>();
onDamageDealt.Register((attacker, damage) =>
{
Console.WriteLine($"{attacker} dealt {damage} damage!");
});
onDamageDealt.Trigger("Player", 50);
```
### EasyEvents
全局事件管理器,提供类型安全的事件注册和获取。
**核心方法:**
```csharp
static void Register<T>() where T : IEvent, new(); // 注册事件类型
static T Get<T>() where T : IEvent, new(); // 获取事件实例
static T GetOrAddEvent<T>() where T : IEvent, new(); // 获取或创建事件实例
```
**使用示例:**
```csharp
// 注册全局事件类型
EasyEvents.Register<GameStartEvent>();
// 获取事件实例
var gameStartEvent = EasyEvents.Get<GameStartEvent>();
// 注册监听
gameStartEvent.Register(() =>
{
Console.WriteLine("Game started!");
});
// 触发事件
gameStartEvent.Trigger();
```
### EventBus
类型化事件系统,支持基于类型的事件发送和注册。这是架构中默认的事件总线实现。
**核心方法:**
```csharp
IUnRegister Register<T>(Action<T> onEvent); // 注册类型化事件
void Send<T>(T e); // 发送事件实例
void Send<T>() where T : new(); // 发送事件(自动创建实例)
void UnRegister<T>(Action<T> onEvent); // 注销事件监听器
```
**使用示例:**
```csharp
// 使用全局事件系统
var eventBus = new EventBus();
// 注册类型化事件
eventBus.Register<PlayerDiedEvent>(e =>
eventBus.Register<PlayerJoinedEvent>(e =>
{
Console.WriteLine($"Player died at position: {e.Position}");
Console.WriteLine(e.Name);
});
// 发送事件(传递实例)
eventBus.Send(new PlayerDiedEvent
{
Position = new Vector3(10, 0, 5)
});
// 发送事件(自动创建实例)
eventBus.Send<PlayerDiedEvent>();
// 注销事件监听器
eventBus.UnRegister<PlayerDiedEvent>(OnPlayerDied);
eventBus.Send(new PlayerJoinedEvent("Alice"));
```
### DefaultUnRegister
如果你还需要统计、过滤或弱引用订阅,可以改用 `EnhancedEventBus`。它在 `EventBus` 基础上额外提供:
默认注销器实现,封装注销回调。
- `Statistics`
- `SendFilterable(...)` / `RegisterFilterable(...)`
- `SendWeak(...)` / `RegisterWeak(...)`
**使用示例:**
这类能力更适合工具层、编辑器层或长生命周期对象,不必默认扩散到每个业务事件。
## 优先级、传播与上下文事件
当事件处理顺序或“是否继续传播”本身就是语义的一部分时,使用优先级入口:
```csharp
Action onUnregister = () => Console.WriteLine("Unregistered");
var unregister = new DefaultUnRegister(onUnregister);
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Events;
// 执行注销
unregister.UnRegister();
```
public sealed record InputCommand(string Name);
### OrEvent
var eventBus = new EventBus();
事件或运算组合器,当任意一个事件触发时触发。
**核心方法:**
```csharp
OrEvent Or(IEvent @event); // 添加要组合的事件
```
**使用示例:**
```csharp
var onAnyInput = new OrEvent()
.Or(onKeyPressed)
.Or(onMouseClicked)
.Or(onTouchDetected);
// 当上述任意事件触发时,执行回调
onAnyInput.Register(() =>
eventBus.RegisterWithContext<InputCommand>(ctx =>
{
Console.WriteLine("Input detected!");
});
```
### UnRegisterList
批量管理注销对象的列表。
**核心方法:**
```csharp
void Add(IUnRegister unRegister); // 添加注销器到列表
void UnRegisterAll(); // 批量注销所有事件
```
**使用示例:**
```csharp
var unregisterList = new UnRegisterList();
// 添加到列表
someEvent.Register(OnEvent).AddToUnregisterList(unregisterList);
// 批量注销
unregisterList.UnRegisterAll();
```
### ArchitectureEvents
定义了架构生命周期相关的事件。
**包含事件:**
- `ArchitectureLifecycleReadyEvent` - 架构生命周期准备就绪
- `ArchitectureDestroyingEvent` - 架构销毁中
- `ArchitectureDestroyedEvent` - 架构已销毁
- `ArchitectureFailedInitializationEvent` - 架构初始化失败
## 在架构中使用事件
### 定义事件类
```csharp
// 简单事件
public struct GameStartedEvent { }
// 带数据的事件
public struct PlayerDiedEvent
{
public Vector3 Position;
public string Cause;
}
// 复杂事件
public struct LevelCompletedEvent
{
public int LevelId;
public float CompletionTime;
public int Score;
public List<string> Achievements;
}
```
### Model 中发送事件
```csharp
public class PlayerModel : AbstractModel
{
public BindableProperty<int> Health { get; } = new(100);
protected override void OnInit()
if (ctx.Data.Name == "Pause")
{
// 监听生命值变化
Health.Register(newHealth =>
{
if (newHealth <= 0)
{
// 发送玩家死亡事件
this.SendEvent(new PlayerDiedEvent
{
Position = Position,
Cause = "Health depleted"
});
}
});
Console.WriteLine("Pause handled");
ctx.MarkAsHandled();
}
}
}, priority: 10);
eventBus.Send(new InputCommand("Pause"), EventPropagation.UntilHandled);
```
### System 中发送事件
当前公开语义是:
```csharp
public class CombatSystem : AbstractSystem
{
protected override void OnInit() { }
public void DealDamage(Character attacker, Character target, int damage)
{
target.Health -= damage;
// 发送伤害事件
this.SendEvent(new DamageDealtEvent
{
Attacker = attacker.Name,
Target = target.Name,
Damage = damage
});
}
}
```
- `Register<T>(handler, priority)`:按优先级订阅
- `RegisterWithContext<T>(...)`:拿到 `EventContext<T>`
- `EventPropagation.All`:广播给全部监听器
- `EventPropagation.UntilHandled`:直到上下文事件被标记为 handled
- `EventPropagation.Highest`:只执行最高优先级层
### Controller 中注册事件
## 局部事件对象
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
如果事件只在一个对象或一个小模块内部流动,不必一定挂到 `EventBus`。当前仍可直接使用:
[ContextAware]
public partial class GameController : IController
{
private IUnRegisterList _unregisterList = new UnRegisterList();
- `EasyEvent`
- `Event<T>`
- `Event<T1, T2>`
- `OrEvent`
- `EventListenerScope<TEvent>`
public void Initialize()
{
// 注册多个事件
this.RegisterEvent<GameStartedEvent>(OnGameStarted)
.AddToUnregisterList(_unregisterList);
这类类型更适合局部组合和 UI/工具层内聚逻辑,不适合作为全局消息总线的替代品。
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied)
.AddToUnregisterList(_unregisterList);
## 与 Store / CQRS 的边界
this.RegisterEvent<LevelCompletedEvent>(OnLevelCompleted)
.AddToUnregisterList(_unregisterList);
}
- 轻量运行时广播:`EventBus`
- 聚合状态演进:`Store<TState>`,必要时用 `BridgeToEventBus(...)` 兼容旧事件消费者
- 新业务请求模型:`GFramework.Cqrs`
private void OnGameStarted(GameStartedEvent e)
{
Console.WriteLine("Game started!");
}
private void OnPlayerDied(PlayerDiedEvent e)
{
Console.WriteLine($"Player died at {e.Position}: {e.Cause}");
ShowGameOverScreen();
}
private void OnLevelCompleted(LevelCompletedEvent e)
{
Console.WriteLine($"Level {e.LevelId} completed! Score: {e.Score}");
ShowVictoryScreen(e);
}
public void Cleanup()
{
_unregisterList.UnRegisterAll();
}
}
```
## 高级用法
### 1. 事件链式组合
```csharp
// 使用 Or 组合多个事件
var onAnyDamage = new OrEvent()
.Or(onPhysicalDamage)
.Or(onMagicDamage)
.Or(onPoisonDamage);
onAnyDamage.Register(() =>
{
PlayDamageSound();
});
```
### 2. 事件过滤
```csharp
// 只处理高伤害事件
this.RegisterEvent<DamageDealtEvent>(e =>
{
if (e.Damage >= 50)
{
ShowCriticalHitEffect();
}
});
```
### 3. 事件转发
```csharp
public class EventBridge : AbstractSystem
{
protected override void OnInit()
{
// 将内部事件转发为公共事件
this.RegisterEvent<InternalPlayerDiedEvent>(e =>
{
this.SendEvent(new PublicPlayerDiedEvent
{
PlayerId = e.Id,
Timestamp = DateTime.UtcNow
});
});
}
}
```
### 4. 临时事件监听
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class TutorialController : IController
{
public void Initialize()
{
// 只监听一次
IUnRegister unregister = null;
unregister = this.RegisterEvent<FirstEnemyKilledEvent>(e =>
{
ShowTutorialComplete();
unregister?.UnRegister(); // 立即注销
});
}
}
```
### 5. 条件事件
```csharp
public class AchievementSystem : AbstractSystem
{
private int _killCount = 0;
protected override void OnInit()
{
this.RegisterEvent<EnemyKilledEvent>(e =>
{
_killCount++;
// 条件满足时发送成就事件
if (_killCount >= 100)
{
this.SendEvent(new AchievementUnlockedEvent
{
AchievementId = "kill_100_enemies"
});
}
});
}
}
```
## 生命周期管理
### 使用 UnRegisterList
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class MyController : IController
{
// 统一管理所有注销对象
private IUnRegisterList _unregisterList = new UnRegisterList();
public void Initialize()
{
// 所有注册都添加到列表
this.RegisterEvent<Event1>(OnEvent1)
.AddToUnregisterList(_unregisterList);
this.RegisterEvent<Event2>(OnEvent2)
.AddToUnregisterList(_unregisterList);
}
public void Cleanup()
{
// 一次性注销所有
_unregisterList.UnRegisterAll();
}
}
```
## 最佳实践
1. **事件命名规范**
- 使用过去式:`PlayerDiedEvent``LevelCompletedEvent`
- 使用 `Event` 后缀:便于识别
- 使用结构体:减少内存分配
2. **事件数据设计**
- 只包含必要信息
- 使用值类型struct提高性能
- 避免传递可变引用
3. **避免事件循环**
- 事件处理器中谨慎发送新事件
- 使用命令打破循环依赖
4. **合理使用事件**
- 用于通知状态变化
- 用于跨模块通信
- 不用于返回数据(使用 Query
5. **注销管理**
- 始终注销事件监听
- 使用 `IUnRegisterList` 批量管理
- 在适当的生命周期点调用 `Cleanup()`
6. **性能考虑**
- 避免频繁触发的事件(如每帧)
- 事件处理器保持轻量
- 使用结构体事件减少 GC
7. **事件设计原则**
- 高内聚:事件应该代表一个完整的业务概念
- 低耦合:事件发送者不需要知道接收者
- 可测试:事件应该易于模拟和测试
## 事件 vs 其他通信方式
| 方式 | 适用场景 | 优点 | 缺点 |
|----------------------|--------------|-----------|---------|
| **Event** | 状态变化通知、跨模块通信 | 松耦合、一对多 | 难以追踪调用链 |
| **Command** | 执行操作、修改状态 | 封装逻辑、可撤销 | 单向通信 |
| **Query** | 查询数据 | 职责清晰、有返回值 | 同步调用 |
| **BindableProperty** | UI 数据绑定 | 自动更新、响应式 | 仅限单一属性 |
## 事件系统架构
事件系统在 GFramework 中的架构位置:
```
Architecture (架构核心)
├── EventBus (事件总线)
├── CommandBus (命令总线)
├── QueryBus (查询总线)
└── IocContainer (IoC容器)
Components (组件)
├── Model (发送事件)
├── System (发送/接收事件)
└── Controller (接收事件)
```
## 相关包
- [`architecture`](./architecture.md) - 提供全局事件系统
- [`extensions`](./extensions.md) - 提供事件扩展方法
- [`property`](./property.md) - 可绑定属性基于事件实现
- **Controller** - 控制器监听事件(接口定义在 Core.Abstractions 中)
- [`model`](./model.md) - 模型发送事件
- [`system`](./system.md) - 系统发送和监听事件
- [`command`](./command.md) - 与事件配合实现 CQRS
- [`query`](./query.md) - 与事件配合实现 CQRS
一个简单判断规则是:如果你关心“谁来处理、是否有返回值、是否要挂 pipeline”用 CQRS如果你只是广播
“这件事发生了”,事件系统更直接。

View File

@ -88,7 +88,7 @@ public sealed class CounterArchitecture : Architecture
- 要接入游戏内容配置、设置、数据仓库、Scene 或 UI
- 转到 [Game](../game/index.md)
- 要接入 Godot 节点、场景和项目元数据生成:
- 转到 `Godot` 与 Source Generators 栏目
- 转到 [Godot](../godot/index.md) 与 [Source Generators](../source-generators/index.md) 栏目
## 推荐阅读顺序

View File

@ -1,364 +1,86 @@
# Logging 包使用说明
# Logging
## 概述
`GFramework.Core.Logging` 是 Core runtime 的默认日志实现。只加载抽象层时,`LoggerFactoryResolver` 会退回
silent provider加载 `GFramework.Core` 或在 `ArchitectureConfiguration` 里显式提供 provider 后,日志才会
真正输出。
Logging 包提供了灵活的日志系统,支持多级别日志记录。默认日志级别为 `Info`,确保框架的关键操作都能被记录下来。
## 核心接口
### ILogger
日志记录器接口,定义了日志记录的基本功能。
**核心方法:**
## 最小用法
```csharp
// 日志级别检查
bool IsTraceEnabled();
bool IsDebugEnabled();
bool IsInfoEnabled();
bool IsWarnEnabled();
bool IsErrorEnabled();
bool IsFatalEnabled();
using GFramework.Core.Abstractions.Logging;
// 记录日志
void Trace(string msg);
void Trace(string format, object arg);
void Trace(string format, object arg1, object arg2);
void Trace(string format, params object[] arguments);
void Trace(string msg, Exception t);
var logger = LoggerFactoryResolver.Provider.CreateLogger("Bootstrap");
void Debug(string msg);
void Debug(string format, object arg);
void Debug(string format, object arg1, object arg2);
void Debug(string format, params object[] arguments);
void Debug(string msg, Exception t);
void Info(string msg);
void Info(string format, object arg);
void Info(string format, object arg1, object arg2);
void Info(string format, params object[] arguments);
void Info(string msg, Exception t);
void Warn(string msg);
void Warn(string format, object arg);
void Warn(string format, object arg1, object arg2);
void Warn(string format, params object[] arguments);
void Warn(string msg, Exception t);
void Error(string msg);
void Error(string format, object arg);
void Error(string format, object arg1, object arg2);
void Error(string format, params object[] arguments);
void Error(string msg, Exception t);
void Fatal(string msg);
void Fatal(string format, object arg);
void Fatal(string format, object arg1, object arg2);
void Fatal(string format, params object[] arguments);
void Fatal(string msg, Exception t);
// 获取日志器名称
string Name();
logger.Info("Application started");
logger.Warn("Config file missing");
```
### ILoggerFactory
默认 `ArchitectureConfiguration` 会把 provider 配成 `ConsoleLoggerFactoryProvider`,最小级别是 `Info`。如果你
直接走标准 `Architecture` 启动路径,这条配置会自动生效。
日志工厂接口,用于创建日志记录器实例。
**核心方法:**
## 在 Architecture 中调整日志级别
```csharp
ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info);
```
using GFramework.Core.Architectures;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Properties;
using GFramework.Core.Logging;
### ILoggerFactoryProvider
日志工厂提供程序接口,用于获取日志工厂。
**核心方法:**
```csharp
ILoggerFactory GetLoggerFactory();
ILogger CreateLogger(string name);
```
### LogLevel
日志级别枚举。
```csharp
public enum LogLevel
var configuration = new ArchitectureConfiguration
{
Trace = 0, // 最详细的跟踪信息
Debug = 1, // 调试信息
Info = 2, // 一般信息(默认级别)
Warning = 3, // 警告信息
Error = 4, // 错误信息
Fatal = 5 // 致命错误
}
```
## 核心类
### AbstractLogger
抽象日志基类,封装了日志级别判断、格式化与异常处理逻辑。平台日志器只需实现 `Write` 方法即可。
**使用示例:**
```csharp
public class CustomLogger : AbstractLogger
{
public CustomLogger(string? name = null, LogLevel minLevel = LogLevel.Info)
: base(name, minLevel)
LoggerProperties = new LoggerProperties
{
}
protected override void Write(LogLevel level, string message, Exception? exception)
{
// 自定义日志输出逻辑
var logMessage = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
if (exception != null)
logMessage += $"\n{exception}";
Console.WriteLine(logMessage);
}
}
```
### ConsoleLogger
控制台日志记录器实现,支持彩色输出。
**使用示例:**
```csharp
// 创建控制台日志记录器
var logger = new ConsoleLogger("MyLogger", LogLevel.Debug);
// 记录不同级别的日志
logger.Info("应用程序启动");
logger.Debug("调试信息");
logger.Warn("警告信息");
logger.Error("错误信息");
logger.Fatal("致命错误");
```
**输出格式:**
```
[2025-01-09 01:40:00.000] INFO [MyLogger] 应用程序启动
[2025-01-09 01:40:01.000] DEBUG [MyLogger] 调试信息
[2025-01-09 01:40:02.000] WARN [MyLogger] 警告信息
```
**日志级别颜色:**
- **Trace**: 深灰色
- **Debug**: 青色
- **Info**: 白色
- **Warning**: 黄色
- **Error**: 红色
- **Fatal**: 洋红色
**构造函数参数:**
- `name`:日志器名称,默认为 "ROOT"
- `minLevel`:最低日志级别,默认为 LogLevel.Info
- `writer`TextWriter 输出流,默认为 Console.Out
- `useColors`:是否使用颜色,默认为 true仅在输出到控制台时生效
### ConsoleLoggerFactory
控制台日志工厂,用于创建控制台日志记录器实例。
**使用示例:**
```csharp
var factory = new ConsoleLoggerFactory();
var logger = factory.GetLogger("MyModule", LogLevel.Debug);
logger.Info("日志记录器创建成功");
```
### ConsoleLoggerFactoryProvider
控制台日志工厂提供程序实现。
**使用示例:**
```csharp
var provider = new ConsoleLoggerFactoryProvider();
provider.MinLevel = LogLevel.Debug; // 设置最低日志级别
var logger = provider.CreateLogger("MyApp");
logger.Info("应用程序启动");
```
### LoggerFactoryResolver
日志工厂提供程序解析器,用于管理和提供日志工厂提供程序实例。
**使用示例:**
```csharp
// 设置日志工厂提供程序
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
// 设置最小日志级别
LoggerFactoryResolver.MinLevel = LogLevel.Debug;
// 获取日志记录器
var logger = LoggerFactoryResolver.Provider.CreateLogger("MyApp");
logger.Info("应用程序启动");
```
## 在架构中使用日志
### 1. 在 Architecture 中使用
```csharp
public class GameArchitecture : Architecture
{
protected override void Init()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture");
logger.Info("游戏架构初始化开始");
RegisterModel(new PlayerModel());
RegisterSystem(new GameSystem());
logger.Info("游戏架构初始化完成");
}
}
```
### 2. 在 System 中使用
```csharp
public class CombatSystem : AbstractSystem
{
protected override void OnInit()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
logger.Info("战斗系统初始化完成");
}
protected override void OnDestroy()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
logger.Info("战斗系统已销毁");
}
}
```
### 3. 在 Model 中使用
```csharp
public class PlayerModel : AbstractModel
{
protected override void OnInit()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("PlayerModel");
logger.Info("玩家模型初始化完成");
}
}
```
### 4. 自定义日志级别
```
public class DebugLogger : AbstractLogger
{
public DebugLogger() : base("Debug", LogLevel.Debug)
{
}
protected override void Write(LogLevel level, string message, Exception? exception)
{
// 只输出调试及更高级别的日志
if (level >= LogLevel.Debug)
LoggerFactoryProvider = new ConsoleLoggerFactoryProvider
{
Console.WriteLine($"[{level}] {message}");
if (exception != null)
Console.WriteLine(exception);
MinLevel = LogLevel.Debug
}
}
};
```
如果你只是想减少噪音或临时打开 `Debug`,通常只调 `MinLevel` 就够了。
## 结构化日志与上下文
默认 Core logger 实现支持 `IStructuredLogger``LogContext`。当你需要把 `requestId``sceneName` 之类的
上下文随异步流透传时,优先用上下文属性,而不是把所有信息拼进字符串。
```csharp
using GFramework.Core.Abstractions.Logging;
var logger = LoggerFactoryResolver.Provider.CreateLogger("Matchmaking");
using (LogContext.Push("RequestId", requestId))
{
if (logger is IStructuredLogger structured)
{
structured.Log(
LogLevel.Info,
"Player matched",
("PlayerId", playerId),
("RoomId", roomId));
}
}
```
## 日志级别说明
## 当前仓库内置的常用实现
| 级别 | 说明 | 使用场景 |
|-------------|----------|-------------------|
| **Trace** | 最详细的跟踪信息 | 调试复杂的执行流程,记录函数调用等 |
| **Debug** | 调试信息 | 开发阶段,记录变量值、流程分支等 |
| **Info** | 一般信息 | 记录重要的业务流程和系统状态 |
| **Warning** | 警告信息 | 可能的问题但不中断程序执行 |
| **Error** | 错误信息 | 影响功能但不致命的问题 |
| **Fatal** | 致命错误 | 导致程序无法继续运行的严重错误 |
- `ConsoleLoggerFactoryProvider`
- `ConsoleLoggerFactory`
- `CompositeLogger`
- `LoggingConfigurationLoader`
## 最佳实践
如果你需要文件输出、rolling file、async appender 或 JSON formatter可以先用
`LoggingConfigurationLoader` 读取 `LoggingConfiguration`,再把自定义 `ILoggerFactoryProvider` 挂到
`ArchitectureConfiguration.LoggerProperties.LoggerFactoryProvider``LoggerFactoryResolver.Provider`
1. **使用合适的日志级别**
- 使用 `Info` 记录重要业务流程
- 使用 `Debug` 记录调试信息
- 使用 `Warning` 记录异常情况
- 使用 `Error` 记录错误但不影响程序运行
- 使用 `Fatal` 记录严重错误
## 什么时候该换 provider
2. **提供上下文信息**
```csharp
logger.Info($"用户登录成功: UserId={userId}, UserName={userName}");
```
下面这些场景通常不该只靠改 `MinLevel`
3. **异常日志记录**
```csharp
try
{
// 业务逻辑
}
catch (Exception ex)
{
logger.Error("数据库操作失败", ex);
}
```
- 需要文件输出、rolling file 或 async appender
- 需要按 namespace / level 做过滤
- 需要 JSON 格式日志
- 需要组合多个 appender
4. **分类使用日志**
```csharp
var dbLogger = LoggerFactoryResolver.Provider.CreateLogger("Database");
var netLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
dbLogger.Info("查询用户数据");
netLogger.Debug("发送HTTP请求");
```
5. **在框架组件中合理使用日志**
```csharp
// 在系统初始化时记录
var logger = LoggerFactoryResolver.Provider.CreateLogger("System");
logger.Info("系统初始化完成");
```
## 注意事项
1. **日志级别检查**
- 每个日志方法都会自动检查日志级别
- 如果当前级别低于最小级别,不会输出日志
2. **格式化参数**
- 支持字符串格式化参数
- 支持异常信息传递
3. **ConsoleLogger 的额外参数**
- ConsoleLogger 现在支持自定义TextWriter输出流
- 支持禁用颜色输出的功能useColors参数
## 相关包
- [architecture](./architecture.md) - 架构核心,使用日志系统记录生命周期事件
- [property](./property.md) - 可绑定属性基于事件系统实现
- [extensions](./extensions.md) - 提供便捷的扩展方法
---
**许可证**: Apache 2.0
这时更合理的做法是保留 `ILogger` 调用面不变,只替换 provider / factory / formatter / appender 组合。

View File

@ -1,477 +1,97 @@
# Property 包使用说明
# Property
## 概述
`GFramework.Core.Property` 负责字段级响应式值。它最适合“一个字段变化就足以驱动视图或局部业务逻辑”的场景;
如果你的状态已经是聚合状态树、需要 reducer / middleware / history再切到
[state-management](./state-management.md)。
Property 包提供了可绑定属性BindableProperty的实现支持属性值的监听和响应式编程。这是实现数据绑定和响应式编程的核心组件。
## 安装方式
BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事件机制实现属性变化的通知。
> 对于简单字段和局部 UI 绑定,`BindableProperty<T>` 仍然是首选方案。
> 如果你需要统一管理复杂状态树、通过 action / reducer 演进状态,或复用局部状态选择器,
> 请同时参考 [`state-management`](./state-management)。
## 核心接口
### IReadonlyBindableProperty`<T>`
只读可绑定属性接口,提供属性值的读取和变更监听功能。
**核心成员:**
```csharp
// 获取属性值
T Value { get; }
// 注册监听(不立即触发回调)
IUnRegister Register(Action<T> onValueChanged);
// 注册监听并立即触发回调传递当前值
IUnRegister RegisterWithInitValue(Action<T> action);
// 取消监听
void UnRegister(Action<T> onValueChanged);
```bash
dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions
```
### IBindableProperty`<T>`
## 最常用类型
可绑定属性接口,继承自只读接口,增加了修改能力。
当前最常见的公开类型是:
**核心成员:**
- `IReadonlyBindableProperty<T>`
- `IBindableProperty<T>`
- `BindableProperty<T>`
一般做法是:内部持有 `BindableProperty<T>`,对外只暴露 `IReadonlyBindableProperty<T>`
## 最小示例
```csharp
// 可读写的属性值
new T Value { get; set; }
using GFramework.Core.Property;
using GFramework.Core.Abstractions.Property;
using GFramework.Core.Model;
// 设置值但不触发事件
void SetValueWithoutEvent(T newValue);
```
## 核心类
### BindableProperty`<T>`
可绑定属性的完整实现。
**核心方法:**
```csharp
// 构造函数
BindableProperty(T defaultValue = default!);
// 属性值
T Value { get; set; }
// 注册监听
IUnRegister Register(Action<T> onValueChanged);
IUnRegister RegisterWithInitValue(Action<T> action);
// 取消监听
void UnRegister(Action<T> onValueChanged);
// 设置值但不触发事件
void SetValueWithoutEvent(T newValue);
// 设置自定义比较器
BindableProperty<T> WithComparer(Func<T, T, bool> comparer);
```
**使用示例:**
```csharp
// 创建可绑定属性
var health = new BindableProperty<int>(100);
// 监听值变化(不会立即触发)
var unregister = health.Register(newValue =>
public sealed class PlayerModel : AbstractModel
{
Console.WriteLine($"Health changed to: {newValue}");
});
// 设置值(会触发监听器)
health.Value = 50; // 输出: Health changed to: 50
// 取消监听
unregister.UnRegister();
// 设置值但不触发事件
health.SetValueWithoutEvent(75);
```
**高级功能:**
```csharp
// 1. 注册并立即获得当前值
health.RegisterWithInitValue(value =>
{
Console.WriteLine($"Current health: {value}"); // 立即输出当前值
// 后续值变化时也会调用
});
// 2. 自定义比较器(静态方法)
BindableProperty<int>.Comparer = (a, b) => Math.Abs(a - b) < 1;
// 3. 使用实例方法设置比较器
var position = new BindableProperty<Vector3>(Vector3.Zero)
.WithComparer((a, b) => a.DistanceTo(b) < 0.01f); // 距离小于0.01认为相等
// 4. 字符串比较器示例
var name = new BindableProperty<string>("Player")
.WithComparer((a, b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase));
```
### BindablePropertyUnRegister`<T>`
可绑定属性的注销器,负责清理监听。
**使用示例:**
```csharp
var unregister = health.Register(OnHealthChanged);
// 当需要取消监听时
unregister.UnRegister();
```
## BindableProperty 工作原理
BindableProperty 基于事件系统实现属性变化通知:
1. **值设置**:当设置 `Value` 属性时,首先进行值比较
2. **变化检测**:使用 `EqualityComparer<T>.Default` 或自定义比较器检测值变化
3. **事件触发**:如果值发生变化,调用所有注册的回调函数
4. **内存管理**:通过 `IUnRegister` 机制管理监听器的生命周期
## 在 Model 中使用
### 什么时候继续使用 BindableProperty
以下场景仍然优先推荐 `BindableProperty<T>`
- 单个字段变化就能驱动视图更新
- 状态范围局限在单个 Model 内
- 不需要统一的 action / reducer 写入入口
- 不需要从聚合状态树中复用局部选择逻辑
如果你的状态已经演化为“多个字段必须一起更新”或“多个模块共享同一聚合状态”,
可以在 Model 内部组合 `Store<TState>`,而不是把所有字段都继续拆成独立属性。
### 与 Store / StateMachine 的边界
- `BindableProperty<T>`:字段级响应式值
- `Store<TState>`:聚合状态容器,负责统一归约状态变化
- `StateMachine`:流程状态切换,不负责数据状态归约
一个复杂 Model 可以同时持有 Store 和 BindableProperty
```csharp
public class PlayerStateModel : AbstractModel
{
public Store<PlayerState> Store { get; } = new(new PlayerState(100, "Player"));
public BindableProperty<bool> IsDirty { get; } = new(false);
protected override void OnInit()
{
Store.RegisterReducer<DamageAction>((state, action) =>
state with { Health = Math.Max(0, state.Health - action.Amount) });
}
}
public sealed record PlayerState(int Health, string Name);
public sealed record DamageAction(int Amount);
```
### 定义可绑定属性
```csharp
public class PlayerModel : AbstractModel
{
// 可读写属性
public BindableProperty<string> Name { get; } = new("Player");
public BindableProperty<int> Level { get; } = new(1);
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> MaxHealth { get; } = new(100);
public BindableProperty<Vector3> Position { get; } = new(Vector3.Zero);
// 只读属性(外部只能读取和监听)
public IReadonlyBindableProperty<int> ReadonlyHealth => Health;
protected override void OnInit()
public void Damage(int amount)
{
// 内部监听属性变化
Health.Register(hp =>
{
if (hp <= 0)
{
this.SendEvent(new PlayerDiedEvent());
}
else if (hp < MaxHealth.Value * 0.3f)
{
this.SendEvent(new LowHealthWarningEvent());
}
});
// 监听等级变化
Level.Register(newLevel =>
{
this.SendEvent(new PlayerLevelUpEvent { NewLevel = newLevel });
});
}
// 业务方法
public void TakeDamage(int damage)
{
Health.Value = Math.Max(0, Health.Value - damage);
}
public void Heal(int amount)
{
Health.Value = Math.Min(MaxHealth.Value, Health.Value + amount);
}
public float GetHealthPercentage()
{
return (float)Health.Value / MaxHealth.Value;
Health.Value = Math.Max(0, Health.Value - amount);
}
}
```
## 在 Controller 中监听
### UI 数据绑定
监听方式:
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class PlayerUI : Control, IController
var unRegister = playerModel.ReadonlyHealth.RegisterWithInitValue(health =>
{
[Export] private Label _healthLabel;
[Export] private Label _nameLabel;
[Export] private ProgressBar _healthBar;
private IUnRegisterList _unregisterList = new UnRegisterList();
public override void _Ready()
{
var playerModel = this.GetModel<PlayerModel>();
// 绑定生命值到UI立即显示当前值
playerModel.Health
.RegisterWithInitValue(health =>
{
_healthLabel.Text = $"HP: {health}/{playerModel.MaxHealth.Value}";
_healthBar.Value = (float)health / playerModel.MaxHealth.Value * 100;
})
.AddToUnregisterList(_unregisterList);
// 绑定最大生命值
playerModel.MaxHealth
.RegisterWithInitValue(maxHealth =>
{
_healthBar.MaxValue = maxHealth;
})
.AddToUnregisterList(_unregisterList);
// 绑定名称
playerModel.Name
.RegisterWithInitValue(name =>
{
_nameLabel.Text = name;
})
.AddToUnregisterList(_unregisterList);
// 绑定位置(仅用于调试显示)
playerModel.Position
.RegisterWithInitValue(pos =>
{
// 仅在调试模式下显示
#if DEBUG
Console.WriteLine($"Player position: {pos}");
#endif
})
.AddToUnregisterList(_unregisterList);
}
public override void _ExitTree()
{
_unregisterList.UnRegisterAll();
}
}
Console.WriteLine($"Current HP: {health}");
});
```
## 常见使用模式
## 当前公开语义
### 1. 双向绑定
- `Value`
- 读写当前值;只有值被判定为“真的变化”时才会触发回调
- `Register(...)`
- 订阅后续变化,不会立即回放当前值
- `RegisterWithInitValue(...)`
- 先回放当前值,再继续订阅
- `SetValueWithoutEvent(...)`
- 更新值但不触发通知
- `UnRegister(...)`
- 显式移除某个处理器
- `WithComparer(...)`
- 改写值变化判定逻辑
```c#
// Model
public class SettingsModel : AbstractModel
{
public BindableProperty<float> MasterVolume { get; } = new(1.0f);
protected override void OnInit() { }
}
## 一个需要注意的兼容点
// UI Controller
[ContextAware]
public partial class VolumeSlider : HSlider, IController
{
private BindableProperty<float> _volumeProperty;
`BindableProperty<T>.Comparer` 是按闭合泛型 `T` 共享的静态比较器,`WithComparer(...)` 本质上会改写这一共享
比较器。也就是说,多个 `BindableProperty<int>` 实例会观察到同一比较规则;只有当你确定整个 `T` 族都要共享同一
判等语义时,再去改它。
public override void _Ready()
{
_volumeProperty = this.GetModel<SettingsModel>().MasterVolume;
## 什么时候继续用 Property
// Model -> UI
_volumeProperty.RegisterWithInitValue(vol => Value = vol)
.UnRegisterWhenNodeExitTree(this);
下面这些场景仍然优先使用 `BindableProperty<T>`
// UI -> Model
ValueChanged += newValue => _volumeProperty.Value = (float)newValue;
}
}
```
- 单个字段变化就能驱动 UI
- 状态范围局限在单个 Model 或单个页面
- 不需要统一的 action / reducer 写入口
- 不需要撤销/重做、历史快照或中间件
### 2. 计算属性
## 什么时候该切到 Store
```c#
public class PlayerModel : AbstractModel
{
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> MaxHealth { get; } = new(100);
public BindableProperty<float> HealthPercent { get; } = new(1.0f);
protected override void OnInit()
{
// 自动计算百分比
Action updatePercent = () =>
{
HealthPercent.Value = (float)Health.Value / MaxHealth.Value;
};
Health.Register(_ => updatePercent());
MaxHealth.Register(_ => updatePercent());
updatePercent(); // 初始计算
}
}
```
如果状态已经演化为下面这些形态,更适合用 `Store<TState>`
### 3. 属性验证
- 多个字段必须作为一个原子状态一起演进
- 多个模块共享同一聚合状态
- 需要 reducer / middleware / 历史回放
- 需要从整棵状态树中复用局部选择逻辑
```c#
public class PlayerModel : AbstractModel
{
private BindableProperty<int> _health = new(100);
public BindableProperty<int> Health
{
get => _health;
set
{
// 限制范围
var clampedValue = Math.Clamp(value.Value, 0, MaxHealth.Value);
_health.Value = clampedValue;
}
}
public BindableProperty<int> MaxHealth { get; } = new(100);
protected override void OnInit() { }
}
```
迁移时不必一次性抛弃旧绑定风格。当前已经提供:
### 4. 条件监听
- `store.Select(...)`
- `store.ToBindableProperty(...)`
```c#
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class CombatController : Node, IController
{
public override void _Ready()
{
var playerModel = this.GetModel<PlayerModel>();
// 只在生命值低于30%时显示警告
playerModel.Health.Register(hp =>
{
if (hp < playerModel.MaxHealth.Value * 0.3f)
{
ShowLowHealthWarning();
}
else
{
HideLowHealthWarning();
}
}).UnRegisterWhenNodeExitTree(this);
}
}
```
## 性能优化
### 1. 避免频繁触发
```c#
// 使用 SetValueWithoutEvent 批量修改
public void LoadPlayerData(SaveData data)
{
// 临时关闭事件
Health.SetValueWithoutEvent(data.Health);
Mana.SetValueWithoutEvent(data.Mana);
Gold.SetValueWithoutEvent(data.Gold);
// 最后统一触发一次更新事件
this.SendEvent(new PlayerDataLoadedEvent());
}
```
### 2. 自定义比较器
```c#
// 避免浮点数精度问题导致的频繁触发
var position = new BindableProperty<Vector3>()
.WithComparer((a, b) => a.DistanceTo(b) < 0.001f);
```
## 实现原理
### 值变化检测
```c#
// 使用 EqualityComparer<T>.Default 进行比较
if (!EqualityComparer<T>.Default.Equals(value, MValue))
{
MValue = value;
_mOnValueChanged?.Invoke(value);
}
```
### 事件触发机制
```c#
// 当值变化时触发所有注册的回调
_mOnValueChanged?.Invoke(value);
```
## 最佳实践
1. **在 Model 中定义属性** - BindableProperty 主要用于 Model 层
2. **使用只读接口暴露** - 防止外部随意修改
3. **及时注销监听** - 使用 UnRegisterList 或 UnRegisterWhenNodeExitTree
4. **使用 RegisterWithInitValue** - UI 绑定时立即获取初始值
5. **避免循环依赖** - 属性监听器中修改其他属性要小心
6. **使用自定义比较器** - 对于浮点数等需要精度控制的属性
7. **复杂聚合状态使用 Store** - 当多个字段必须统一演进时,使用 Store 管理聚合状态更清晰
## 相关包
- [`model`](./model.md) - Model 中大量使用 BindableProperty
- [`events`](./events.md) - BindableProperty 基于事件系统实现
- [`state-management`](./state-management) - 复杂状态树的集中式管理方案
- [`extensions`](./extensions.md) - 提供便捷的注销扩展方法
---
**许可证**: Apache 2.0
这意味着你可以先把写路径统一到 `Store<TState>`,再渐进迁移现有 UI 或 Controller 的读取方式。

View File

@ -1,658 +1,224 @@
---
title: 场景系统
description: 场景系统提供了完整的场景生命周期管理、路由导航和转换控制功能
description: 说明 GFramework.Game 场景路由的当前入口、项目侧接入职责与扩展边界
---
# 场景系统
## 概述
`GFramework.Game` 的场景系统是“路由基类 + 场景契约 + 过渡管线”的组合,不是替你包办注册表、节点树和引擎对象装配的
一体化方案。
场景系统是 GFramework.Game 中用于管理游戏场景的核心组件。它提供了场景的加载、卸载、切换、暂停和恢复等完整生命周期管理,以及基于栈的场景导航机制。
框架当前负责的是:
通过场景系统,你可以轻松实现场景之间的平滑切换,管理场景栈(如主菜单 -> 游戏 -> 暂停菜单),并在场景转换时执行自定义逻辑。
- 场景栈管理
- `Load -> Enter -> Pause -> Resume -> Exit -> Unload` 生命周期顺序
- 路由守卫与过渡处理器执行时机
- `SceneRouterBase` 这一层的默认切换编排
**主要特性**
项目或引擎适配层仍然需要自己提供
- 完整的场景生命周期管理
- 基于栈的场景导航
- 场景转换管道和钩子
- 路由守卫Route Guard
- 场景工厂和行为模式
- 异步加载和卸载
- `ISceneFactory`
- `ISceneRoot`
- 具体的 `ISceneBehavior` / `IScene`
- 场景键和资源、节点、预制体之间的映射关系
## 核心概念
如果你把它理解为“可复用的场景路由底座”而不是“现成的完整场景框架”,后续接法会更贴近源码。
### 场景接
## 当前公开入
`IScene` 定义了场景的完整生命周期:
### `IScene`
业务场景生命周期契约,描述加载、进入、暂停、恢复、退出、卸载这六个阶段。
### `ISceneBehavior`
路由器直接操作的运行时对象。它除了场景生命周期外,还携带:
- `Key`
- `Original`
- `IsLoaded`
- `IsActive`
- `IsTransitioning`
如果你的引擎对象本身就能承担这些语义,可以直接实现 `ISceneBehavior`。如果你更想把业务逻辑放在纯 C# 场景类中,也可以由
项目侧行为包装器承载真正的引擎节点,再把业务场景逻辑委托出去。
### `ISceneRouter`
当前公开的路由接口,重点入口是:
- `BindRoot(ISceneRoot root)`
- `ReplaceAsync(string sceneKey, ISceneEnterParam? param = null)`
- `PushAsync(string sceneKey, ISceneEnterParam? param = null)`
- `PopAsync()`
- `ClearAsync()`
- `Contains(string sceneKey)`
### `SceneRouterBase`
`GFramework.Game` 提供的默认实现基类。它会:
- 在 `OnInit()` 中获取 `ISceneFactory`
- 通过 `SemaphoreSlim` 串行化切换
- 调用守卫、过渡处理器和环绕处理器
- 维护场景栈与恢复顺序
通常项目不会直接修改框架里的 `SceneRouterBase`,而是在项目层继承它。
## 场景栈的真实语义
按当前实现,最常用的三个动作语义如下:
- `ReplaceAsync`
- 清空整个栈,再加载并进入目标场景。
- `PushAsync`
- 先检查守卫,再创建新场景,挂到 `ISceneRoot`,执行 `OnLoadAsync()`,暂停当前栈顶,最后让新场景 `OnEnterAsync()`
- `PopAsync`
- 对栈顶执行离开检查,通过后退出并卸载它,再从 `ISceneRoot` 移除,然后恢复新的栈顶。
当前还有两个容易被旧文档误导的点:
- `SceneRouterBase` 默认不允许同一个 `sceneKey` 在栈中重复存在;内部会先做 `Contains(sceneKey)` 检查
- 框架不会替你实现“场景键 -> 具体场景实例”的注册逻辑;这仍然是 `ISceneFactory` 或项目注册表的职责
## 最小接入路径
推荐按下面的顺序接入。
### 1. 准备项目自己的 router
```csharp
public interface IScene
using GFramework.Game.Scene;
using LoggingTransitionHandler = GFramework.Game.Scene.Handler.LoggingTransitionHandler;
public sealed class GameSceneRouter : SceneRouterBase
{
ValueTask OnLoadAsync(ISceneEnterParam? param); // 加载资源
ValueTask OnEnterAsync(); // 进入场景
ValueTask OnPauseAsync(); // 暂停场景
ValueTask OnResumeAsync(); // 恢复场景
ValueTask OnExitAsync(); // 退出场景
ValueTask OnUnloadAsync(); // 卸载资源
}
```
### 场景路由
`ISceneRouter` 管理场景的导航和切换:
```csharp
public interface ISceneRouter : ISystem
{
ISceneBehavior? Current { get; } // 当前场景
string? CurrentKey { get; } // 当前场景键
IEnumerable<ISceneBehavior> Stack { get; } // 场景栈
bool IsTransitioning { get; } // 是否正在切换
ValueTask ReplaceAsync(string sceneKey, ISceneEnterParam? param = null);
ValueTask PushAsync(string sceneKey, ISceneEnterParam? param = null);
ValueTask PopAsync();
ValueTask ClearAsync();
}
```
### 场景行为
`ISceneBehavior` 封装了场景的具体实现和引擎集成:
```csharp
public interface ISceneBehavior
{
string Key { get; } // 场景唯一标识
IScene Scene { get; } // 场景实例
ValueTask LoadAsync(ISceneEnterParam? param);
ValueTask UnloadAsync();
}
```
## 基本用法
### 定义场景
实现 `IScene` 接口创建场景:
```csharp
using GFramework.Game.Abstractions.Scene;
public class MainMenuScene : IScene
{
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
protected override void RegisterHandlers()
{
// 加载场景资源
Console.WriteLine("加载主菜单资源");
await Task.Delay(100); // 模拟加载
}
public async ValueTask OnEnterAsync()
{
// 进入场景
Console.WriteLine("进入主菜单");
// 显示 UI、播放音乐等
await Task.CompletedTask;
}
public async ValueTask OnPauseAsync()
{
// 暂停场景
Console.WriteLine("暂停主菜单");
await Task.CompletedTask;
}
public async ValueTask OnResumeAsync()
{
// 恢复场景
Console.WriteLine("恢复主菜单");
await Task.CompletedTask;
}
public async ValueTask OnExitAsync()
{
// 退出场景
Console.WriteLine("退出主菜单");
// 隐藏 UI、停止音乐等
await Task.CompletedTask;
}
public async ValueTask OnUnloadAsync()
{
// 卸载场景资源
Console.WriteLine("卸载主菜单资源");
await Task.Delay(50); // 模拟卸载
RegisterHandler(new LoggingTransitionHandler());
}
}
```
### 注册场景
这一步只解决“切换流程怎么跑”,不解决“场景从哪来”。
在场景注册表中注册场景:
### 2. 提供 `ISceneFactory`
`SceneRouterBase` 会在初始化阶段通过 `GetUtility<ISceneFactory>()` 获取工厂,因此项目必须先注册它。
工厂的职责通常是:
- 按 `sceneKey` 找到项目自己的注册表、预制体或资源描述
- 创建或获取 `ISceneBehavior`
- 决定行为对象如何包裹引擎节点与业务场景逻辑
如果项目里已经有场景注册表,也建议把它收口在 factory 内部,而不是让文档继续暗示框架自带统一注册中心。
### 3. 提供 `ISceneRoot`
`ISceneRoot` 只做两件事:
- `AddScene(ISceneBehavior scene)`
- `RemoveScene(ISceneBehavior scene)`
也就是说root 是“挂载/移除容器”,不是路由器本身。当前 `ai-libs/` 参考实现也是在项目自己的 Godot 节点里实现
`ISceneRoot`,并在 `_Ready()` 时调用 `BindRoot(this)`
### 4. 把 router 和 factory 装进架构
```csharp
using GFramework.Game.Abstractions.Scene;
architecture.RegisterUtility<ISceneFactory>(new GameSceneFactory());
architecture.RegisterSystem(new GameSceneRouter());
```
public class GameSceneRegistry : IGameSceneRegistry
如果你的项目还需要动画、黑幕或 loading 过渡,可以继续在 `RegisterHandlers()` 里补自己的处理器。
### 5. 在 root 就绪后绑定
```csharp
public sealed class SceneRoot : Node2D, ISceneRoot
{
private readonly Dictionary<string, Type> _scenes = new();
[GetSystem] private ISceneRouter _sceneRouter = null!;
public GameSceneRegistry()
public override void _Ready()
{
// 注册场景
Register("MainMenu", typeof(MainMenuScene));
Register("Gameplay", typeof(GameplayScene));
Register("Pause", typeof(PauseScene));
__InjectContextBindings_Generated();
_sceneRouter.BindRoot(this);
}
public void Register(string key, Type sceneType)
public void AddScene(ISceneBehavior scene)
{
_scenes[key] = sceneType;
// 项目侧决定如何把 scene.Original 挂进引擎节点树
}
public Type? GetSceneType(string key)
public void RemoveScene(ISceneBehavior scene)
{
return _scenes.TryGetValue(key, out var type) ? type : null;
// 项目侧决定如何移除并释放引擎对象
}
}
```
### 切换场景
使用场景路由进行导航:
### 6. 从业务代码发起导航
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class GameController : IController
{
public async Task StartGame()
await sceneRouter.ReplaceAsync(
"Gameplay",
new GameplayEnterParam
{
var sceneRouter = this.GetSystem<ISceneRouter>();
Seed = "new-game"
});
// 替换当前场景(清空场景栈)
await sceneRouter.ReplaceAsync("Gameplay");
}
public async Task ShowPauseMenu()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
// 压入新场景(保留当前场景)
await sceneRouter.PushAsync("Pause");
}
public async Task ClosePauseMenu()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
// 弹出当前场景(恢复上一个场景)
await sceneRouter.PopAsync();
}
}
await sceneRouter.PushAsync("PauseMenu");
await sceneRouter.PopAsync();
```
## 高级用法
### 场景参数传递
通过 `ISceneEnterParam` 传递数据:
```csharp
// 定义场景参数
public class GameplayEnterParam : ISceneEnterParam
{
public int Level { get; set; }
public string Difficulty { get; set; }
}
// 在场景中接收参数
public class GameplayScene : IScene
{
private int _level;
private string _difficulty;
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
if (param is GameplayEnterParam gameplayParam)
{
_level = gameplayParam.Level;
_difficulty = gameplayParam.Difficulty;
Console.WriteLine($"加载关卡 {_level},难度: {_difficulty}");
}
await Task.CompletedTask;
}
// ... 其他生命周期方法
}
// 切换场景时传递参数
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
{
Level = 1,
Difficulty = "Normal"
});
```
## 扩展点
### 路由守卫
使用路由守卫控制场景切换
如果你要在进入或离开场景前做业务检查,实现 `ISceneRouteGuard`
```csharp
using GFramework.Game.Abstractions.Scene;
- `CanEnterAsync(string sceneKey, ISceneEnterParam? param)`
- `CanLeaveAsync(string sceneKey)`
public class SaveGameGuard : ISceneRouteGuard
{
public async ValueTask<bool> CanLeaveAsync(
ISceneBehavior from,
string toKey,
ISceneEnterParam? param)
{
// 离开游戏场景前检查是否需要保存
if (from.Key == "Gameplay")
{
var needsSave = CheckIfNeedsSave();
if (needsSave)
{
await SaveGameAsync();
}
}
适合放:
return true; // 允许离开
}
- 未保存进度拦截
- 场景解锁条件检查
- 新手引导流程限制
public async ValueTask<bool> CanEnterAsync(
string toKey,
ISceneEnterParam? param)
{
// 进入场景前的验证
if (toKey == "Gameplay")
{
// 检查是否满足进入条件
var canEnter = CheckGameplayRequirements();
return canEnter;
}
### 过渡处理器
return true;
}
`SceneRouterBase` 公开了:
private bool CheckIfNeedsSave() => true;
private async Task SaveGameAsync() => await Task.Delay(100);
private bool CheckGameplayRequirements() => true;
}
- `RegisterHandler(ISceneTransitionHandler handler, SceneTransitionHandlerOptions? options = null)`
- `RegisterAroundHandler(ISceneAroundTransitionHandler handler, SceneTransitionHandlerOptions? options = null)`
// 注册守卫
sceneRouter.AddGuard(new SaveGameGuard());
```
适合放:
### 场景转换处理器
- 日志
- 黑幕、淡入淡出或 loading 动画
- 切场前后的指标采集
自定义场景转换逻辑:
如果你的项目已经有复杂引擎过渡逻辑,优先把这些逻辑放进 handler而不是把 `SceneRouterBase` 派生类本身做成巨型协调器。
```csharp
using GFramework.Game.Abstractions.Scene;
## 与旧写法的边界
public class FadeTransitionHandler : ISceneTransitionHandler
{
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"准备加载场景: {@event.ToKey}");
// 显示加载画面
await ShowLoadingScreen();
}
下面这些说法不再适合作为默认接入指导:
public async ValueTask OnAfterLoadAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"场景加载完成: {@event.ToKey}");
await Task.CompletedTask;
}
- “框架会帮你直接注册和发现所有场景类型”
- “只要写一个 `IScene` 就能自动接入所有引擎对象”
- “场景系统本身自带统一注册表和完整项目结构”
public async ValueTask OnBeforeEnterAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"准备进入场景: {@event.ToKey}");
// 播放淡入动画
await PlayFadeIn();
}
当前更准确的理解是:
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"已进入场景: {@event.ToKey}");
// 隐藏加载画面
await HideLoadingScreen();
}
- 框架提供通用场景切换编排
- 项目提供 factory、root、资源映射和具体引擎装配
- 文档中的最小示例应优先说明职责边界,而不是继续堆叠大而全教程
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"准备退出场景: {@event.FromKey}");
// 播放淡出动画
await PlayFadeOut();
}
## 推荐阅读
public async ValueTask OnAfterExitAsync(SceneTransitionEvent @event)
{
Console.WriteLine($"已退出场景: {@event.FromKey}");
await Task.CompletedTask;
}
private async Task ShowLoadingScreen() => await Task.Delay(100);
private async Task HideLoadingScreen() => await Task.Delay(100);
private async Task PlayFadeIn() => await Task.Delay(200);
private async Task PlayFadeOut() => await Task.Delay(200);
}
// 注册转换处理器
sceneRouter.AddTransitionHandler(new FadeTransitionHandler());
```
### 场景栈管理
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class SceneNavigationController : IController
{
public async Task NavigateToSettings()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
// 检查场景是否已在栈中
if (sceneRouter.Contains("Settings"))
{
Console.WriteLine("设置场景已打开");
return;
}
// 压入设置场景
await sceneRouter.PushAsync("Settings");
}
public void ShowSceneStack()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
Console.WriteLine("当前场景栈:");
foreach (var scene in sceneRouter.Stack)
{
Console.WriteLine($"- {scene.Key}");
}
}
public async Task ReturnToMainMenu()
{
var sceneRouter = this.GetSystem<ISceneRouter>();
// 清空所有场景并加载主菜单
await sceneRouter.ClearAsync();
await sceneRouter.ReplaceAsync("MainMenu");
}
}
```
### 场景加载进度
```csharp
public class GameplayScene : IScene
{
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
var resourceManager = GetResourceManager();
// 加载多个资源并报告进度
var resources = new[]
{
"textures/player.png",
"textures/enemy.png",
"audio/bgm.mp3",
"models/level.obj"
};
for (int i = 0; i < resources.Length; i++)
{
await resourceManager.LoadAsync<object>(resources[i]);
// 报告进度
var progress = (i + 1) / (float)resources.Length;
ReportProgress(progress);
}
}
private void ReportProgress(float progress)
{
// 发送进度事件
Console.WriteLine($"加载进度: {progress * 100:F0}%");
}
// ... 其他生命周期方法
}
```
### 场景预加载
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class PreloadController : IController
{
public async Task PreloadNextLevel()
{
var sceneFactory = this.GetUtility<ISceneFactory>();
// 预加载下一关场景
var scene = sceneFactory.Create("Level2");
await scene.OnLoadAsync(null);
Console.WriteLine("下一关预加载完成");
}
}
```
## 最佳实践
1. **在 OnLoad 中加载资源,在 OnUnload 中释放**:保持资源管理清晰
```csharp
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
{
_texture = await LoadTextureAsync("player.png");
}
public async ValueTask OnUnloadAsync()
{
_texture?.Dispose();
_texture = null;
}
```
2. **使用 Push/Pop 管理临时场景**:如暂停菜单、设置界面
```csharp
// 打开暂停菜单(保留游戏场景)
await sceneRouter.PushAsync("Pause");
// 关闭暂停菜单(恢复游戏场景)
await sceneRouter.PopAsync();
```
3. **使用 Replace 切换主要场景**:如从菜单到游戏
```csharp
// 开始游戏(清空场景栈)
await sceneRouter.ReplaceAsync("Gameplay");
```
4. **在 OnPause/OnResume 中管理状态**:暂停和恢复游戏逻辑
```csharp
public async ValueTask OnPauseAsync()
{
// 暂停游戏逻辑
_gameTimer.Pause();
_audioSystem.PauseBGM();
}
public async ValueTask OnResumeAsync()
{
// 恢复游戏逻辑
_gameTimer.Resume();
_audioSystem.ResumeBGM();
}
```
5. **使用路由守卫处理业务逻辑**:如保存检查、权限验证
```csharp
public async ValueTask<bool> CanLeaveAsync(...)
{
if (HasUnsavedChanges())
{
var confirmed = await ShowSaveDialog();
if (confirmed)
{
await SaveAsync();
}
return confirmed;
}
return true;
}
```
6. **避免在场景切换时阻塞**:使用异步操作
```csharp
✓ await sceneRouter.ReplaceAsync("Gameplay");
✗ sceneRouter.ReplaceAsync("Gameplay").Wait(); // 可能死锁
```
## 常见问题
### 问题Replace、Push、Pop 有什么区别?
**解答**
- **Replace**:清空场景栈,加载新场景(用于主要场景切换)
- **Push**:压入新场景,暂停当前场景(用于临时场景)
- **Pop**:弹出当前场景,恢复上一个场景(用于关闭临时场景)
```csharp
// 场景栈示例
await sceneRouter.ReplaceAsync("MainMenu"); // [MainMenu]
await sceneRouter.PushAsync("Settings"); // [MainMenu, Settings]
await sceneRouter.PushAsync("About"); // [MainMenu, Settings, About]
await sceneRouter.PopAsync(); // [MainMenu, Settings]
await sceneRouter.PopAsync(); // [MainMenu]
```
### 问题:如何在场景之间传递数据?
**解答**
有几种方式:
1. **通过场景参数**
```csharp
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
{
Level = 5
});
```
2. **通过 Model**
```csharp
var gameModel = this.GetModel<GameModel>();
gameModel.CurrentLevel = 5;
await sceneRouter.ReplaceAsync("Gameplay");
```
3. **通过事件**
```csharp
this.SendEvent(new LevelSelectedEvent { Level = 5 });
await sceneRouter.ReplaceAsync("Gameplay");
```
### 问题:场景切换时如何显示加载画面?
**解答**
使用场景转换处理器:
```csharp
public class LoadingScreenHandler : ISceneTransitionHandler
{
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
{
await ShowLoadingScreen();
}
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
{
await HideLoadingScreen();
}
// ... 其他方法
}
```
### 问题:如何防止用户在场景切换时操作?
**解答**
检查 `IsTransitioning` 状态:
```csharp
public async Task ChangeScene(string sceneKey)
{
var sceneRouter = this.GetSystem<ISceneRouter>();
if (sceneRouter.IsTransitioning)
{
Console.WriteLine("场景正在切换中,请稍候");
return;
}
await sceneRouter.ReplaceAsync(sceneKey);
}
```
### 问题:场景切换失败怎么办?
**解答**
使用 try-catch 捕获异常:
```csharp
try
{
await sceneRouter.ReplaceAsync("Gameplay");
}
catch (Exception ex)
{
Console.WriteLine($"场景切换失败: {ex.Message}");
// 回退到安全场景
await sceneRouter.ReplaceAsync("MainMenu");
}
```
### 问题:如何实现场景预加载?
**解答**
在后台预先加载场景资源:
```csharp
// 在当前场景中预加载下一个场景
var factory = this.GetUtility<ISceneFactory>();
var nextScene = factory.Create("NextLevel");
await nextScene.OnLoadAsync(null);
// 稍后快速切换
await sceneRouter.ReplaceAsync("NextLevel");
```
## 相关文档
- [UI 系统](/zh-CN/game/ui) - UI 页面管理
- [资源管理系统](/zh-CN/core/resource) - 场景资源加载
- [状态机系统](/zh-CN/core/state-machine) - 场景状态管理
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 引擎集成
- [存档系统实现教程](/zh-CN/tutorials/save-system) - 场景切换时保存数据
1. [game/index.md](./index.md)
2. [ui.md](./ui.md)
3. `GFramework.Game/README.md`
4. `GFramework.Game.Abstractions/README.md`

View File

@ -1,509 +1,293 @@
---
title: UI 系统
description: UI 系统提供了完整的 UI 页面管理、路由导航和多层级显示功能
description: 说明 GFramework.Game UI 路由当前的页面栈、层级 UI、输入语义与项目侧接入方式
---
# UI 系统
## 概述
`GFramework.Game` 的 UI 系统不是单纯的“页面栈”。按当前实现,它同时覆盖:
UI 系统是 GFramework.Game 中用于管理游戏 UI 界面的核心组件。它提供了 UI 页面的生命周期管理、基于栈的导航机制,以及多层级的
UI 显示系统Page、Overlay、Modal、Toast、Topmost
- `UiLayer.Page` 的页面导航
- `Overlay` / `Modal` / `Toast` / `Topmost` 的层级 UI
- UI 语义动作捕获与分发
- World 输入阻断
- 由 UI 可见性驱动的暂停语义
通过 UI 系统,你可以轻松实现 UI 页面之间的切换,管理 UI 栈(如主菜单 -> 设置 -> 关于),以及在不同层级显示各种类型的
UI对话框、提示、加载界面等
因此,新的接入文档不应再把它写成“只有 Push/Pop 的传统页面管理器”。
**主要特性**
## 当前公开入口
- 完整的 UI 生命周期管理
- 基于栈的 UI 导航
- 多层级 UI 显示5 个层级)
- UI 转换管道和钩子
- 路由守卫Route Guard
- UI 工厂和行为模式
### `IUiPage`
## 核心概念
最轻量的页面生命周期契约,暴露:
### UI 页面接口
- `OnEnter`
- `OnExit`
- `OnPause`
- `OnResume`
- `OnShow`
- `OnHide`
`IUiPage` 定义了 UI 页面的生命周期:
如果你的页面逻辑只想表达这些生命周期阶段,停留在 `IUiPage` 就够了。
### `IUiPageBehavior`
路由器真正操作的运行时页面行为。相比 `IUiPage`,它还携带:
- `Key`
- `Layer`
- `Handle`
- `View`
- `IsAlive`
- `IsVisible`
- `IsModal`
- `BlocksInput`
- `InteractionProfile`
- `TryHandleUiAction(UiInputAction action)`
也就是说,页面栈和层级 UI 都是围绕 `IUiPageBehavior` 工作的,而不是只围绕 `IUiPage`
### `IUiRouter`
当前最常用的入口分成两组。
页面栈:
- `PushAsync(...)`
- `ReplaceAsync(...)`
- `PopAsync(...)`
- `ClearAsync()`
- `Peek()`
- `PeekKey()`
层级 UI
- `Show(...)`
- `Hide(...)`
- `Resume(...)`
- `ClearLayer(...)`
- `HideByKey(...)`
- `GetAllFromLayer(...)`
输入与阻断:
- `GetUiActionOwner(UiInputAction action)`
- `TryDispatchUiAction(UiInputAction action)`
- `BlocksWorldPointerInput()`
- `BlocksWorldActionInput()`
### `UiLayer`
当前层级语义如下:
- `Page`
- 页面栈层。请用 `PushAsync` / `ReplaceAsync`,不要用 `Show(...)`
- `Overlay`
- 可叠加的浮层。
- `Modal`
- 默认阻断下层输入的模态层。
- `Toast`
- 轻量提示层。
- `Topmost`
- 最顶层的系统级 UI。
### `UiTransitionPolicy``UiPopPolicy`
页面栈的两个关键策略:
- `UiTransitionPolicy.Exclusive`
- 新页面独占显示,下层页面会 `Pause + Hide`
- `UiTransitionPolicy.Overlay`
- 新页面覆盖显示,下层页面只 `Pause`
- `UiPopPolicy.Destroy`
- 弹出时直接销毁页面实例
- `UiPopPolicy.Suspend`
- 弹出时保留页面实例,供后续恢复
## UI 路由的真实语义
### 页面栈和层级 UI 是两套入口
当前源码里:
- `Page` 层属于栈语义,用 `PushAsync` / `ReplaceAsync` / `PopAsync`
- `Overlay``Modal``Toast``Topmost` 属于层级语义,用 `Show` / `Hide` / `Resume`
`Show(..., UiLayer.Page)` 在当前实现里会直接抛异常,因此旧文档里那种“所有 UI 都统一通过 Show 进入”的写法不再准确。
### 输入不是页面自己抢,而是 router 先仲裁
`UiInteractionProfile` 用来描述页面的交互契约,例如:
- 捕获哪些 `UiInputAction`
- 是否阻断 World 指针输入
- 是否阻断 World 语义动作输入
- 页面可见时是否推动暂停栈
输入层先把设备输入映射成 `UiInputAction`,再交给 `IUiRouter.TryDispatchUiAction(...)`。最终谁拥有动作捕获权,由当前可见页面和层级顺序决定。
### 页面可见性会影响暂停与阻断
这也是 UI 系统和普通页面栈最不同的地方之一。当前实现里:
- `Modal` / `Topmost` 默认具有更强的输入阻断语义
- 页面的 `InteractionProfile` 可以驱动暂停栈
- `BlocksWorldPointerInput()``BlocksWorldActionInput()` 是给项目输入层做统一判断的
如果你的项目有“打开设置页后暂停世界”“Modal 打开时地图点击失效”这类需求,优先接这个契约,而不是每个页面自己散落地写输入屏蔽逻辑。
## 最小接入路径
### 1. 提供项目自己的 router
```csharp
public interface IUiPage
using GFramework.Game.UI;
using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler;
public sealed class GameUiRouter : UiRouterBase
{
void OnEnter(IUiPageEnterParam? param); // 进入页面
void OnExit(); // 退出页面
void OnPause(); // 暂停页面
void OnResume(); // 恢复页面
void OnShow(); // 显示页面
void OnHide(); // 隐藏页面
}
```
### UI 路由
`IUiRouter` 管理 UI 的导航和切换:
```csharp
public interface IUiRouter : ISystem
{
int Count { get; } // UI 栈深度
IUiPageBehavior? Peek(); // 栈顶 UI
ValueTask PushAsync(string uiKey, IUiPageEnterParam? param = null);
ValueTask PopAsync(UiPopPolicy policy = UiPopPolicy.Destroy);
ValueTask ReplaceAsync(string uiKey, IUiPageEnterParam? param = null);
ValueTask ClearAsync();
}
```
### UI 层级
UI 系统支持 5 个显示层级:
```csharp
public enum UiLayer
{
Page, // 页面层(栈管理,不可重入)
Overlay, // 浮层(可重入,对话框等)
Modal, // 模态层(可重入,带遮罩)
Toast, // 提示层(可重入,轻量提示)
Topmost // 顶层(不可重入,系统级)
}
```
## 基本用法
### 定义 UI 页面
实现 `IUiPage` 接口创建 UI 页面:
```csharp
using GFramework.Game.Abstractions.UI;
public class MainMenuPage : IUiPage
{
public void OnEnter(IUiPageEnterParam? param)
protected override void RegisterHandlers()
{
Console.WriteLine("进入主菜单");
// 初始化 UI、绑定事件
}
public void OnExit()
{
Console.WriteLine("退出主菜单");
// 清理资源、解绑事件
}
public void OnPause()
{
Console.WriteLine("暂停主菜单");
// 暂停动画、停止交互
}
public void OnResume()
{
Console.WriteLine("恢复主菜单");
// 恢复动画、启用交互
}
public void OnShow()
{
Console.WriteLine("显示主菜单");
// 显示 UI 元素
}
public void OnHide()
{
Console.WriteLine("隐藏主菜单");
// 隐藏 UI 元素
RegisterHandler(new LoggingTransitionHandler());
}
}
```
### 切换 UI 页面
### 2. 提供 `IUiFactory`
使用 UI 路由进行导航:
`UiRouterBase` 会通过 `IUiFactory.Create(string uiKey)` 获取页面行为实例,因此项目需要自己决定:
- `uiKey` 如何映射到页面行为
- 页面行为如何包裹具体引擎视图
- 预挂载节点、调试节点或动态实例化页面如何接入
如果你在 Godot 项目里使用 `AutoUiPage` 相关生成器,它可以帮你减少部分行为样板,但 factory / root / 实际页面注册仍然是项目职责。
### 3. 提供 `IUiRoot`
`IUiRoot` 负责把页面行为挂进真实 UI 容器:
- `AddUiPage(IUiPageBehavior child)`
- `AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)`
- `RemoveUiPage(IUiPageBehavior child)`
当前 `ai-libs/` 的参考实现就是在项目自己的 `CanvasLayer` 上为每个 `UiLayer` 建独立容器,再在 `_Ready()` 时执行
`_uiRouter.BindRoot(this)`
### 4. 装配 router 与 factory
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
architecture.RegisterUtility<IUiFactory>(new GameUiFactory());
architecture.RegisterSystem(new GameUiRouter());
```
[ContextAware]
public partial class UiController : IController
### 5. 在 root 就绪后绑定
```csharp
public sealed class UiRoot : CanvasLayer, IUiRoot
{
public async Task ShowSettings()
{
var uiRouter = this.GetSystem<IUiRouter>();
[GetSystem] private IUiRouter _uiRouter = null!;
// 压入设置页面(保留当前页面)
await uiRouter.PushAsync("Settings");
public override void _Ready()
{
__InjectContextBindings_Generated();
_uiRouter.BindRoot(this);
}
public async Task CloseSettings()
public void AddUiPage(IUiPageBehavior child)
{
var uiRouter = this.GetSystem<IUiRouter>();
// 弹出当前页面(返回上一页)
await uiRouter.PopAsync();
AddUiPage(child, UiLayer.Page);
}
public async Task ShowMainMenu()
public void AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)
{
var uiRouter = this.GetSystem<IUiRouter>();
// 项目侧决定如何把 child.View 挂到具体容器
}
// 替换所有页面(清空 UI 栈)
await uiRouter.ReplaceAsync("MainMenu");
public void RemoveUiPage(IUiPageBehavior child)
{
// 项目侧决定如何移除并释放视图
}
}
```
### 显示不同层级的 UI
### 6. 从业务代码区分两类入口
页面栈:
```csharp
[ContextAware]
public partial class UiController : IController
{
public void ShowDialog()
{
var uiRouter = this.GetSystem<IUiRouter>();
// 在 Modal 层显示对话框
var handle = uiRouter.Show("ConfirmDialog", UiLayer.Modal);
}
public void ShowToast(string message)
{
var uiRouter = this.GetSystem<IUiRouter>();
// 在 Toast 层显示提示
var handle = uiRouter.Show("ToastMessage", UiLayer.Toast,
new ToastParam { Message = message });
}
public void ShowLoading()
{
var uiRouter = this.GetSystem<IUiRouter>();
// 在 Topmost 层显示加载界面
var handle = uiRouter.Show("LoadingScreen", UiLayer.Topmost);
}
}
await uiRouter.ReplaceAsync("MainMenu");
await uiRouter.PushAsync("Settings", new SettingsEnterParam());
await uiRouter.PopAsync(UiPopPolicy.Destroy);
```
## 高级用法
### UI 参数传递
层级 UI
```csharp
// 定义 UI 参数
public class SettingsEnterParam : IUiPageEnterParam
{
public string Category { get; set; }
}
var modalHandle = uiRouter.Show(
"ConfirmExit",
UiLayer.Modal,
new ConfirmExitParam());
// 在 UI 中接收参数
public class SettingsPage : IUiPage
{
private string _category;
public void OnEnter(IUiPageEnterParam? param)
{
if (param is SettingsEnterParam settingsParam)
{
_category = settingsParam.Category;
Console.WriteLine($"打开设置分类: {_category}");
}
}
// ... 其他生命周期方法
}
// 传递参数
await uiRouter.PushAsync("Settings", new SettingsEnterParam
{
Category = "Audio"
});
uiRouter.Hide(modalHandle, UiLayer.Modal);
```
## 扩展点
### 路由守卫
```csharp
using GFramework.Game.Abstractions.UI;
如果你要在进入或离开页面前做业务检查,实现 `IUiRouteGuard`
public class UnsavedChangesGuard : IUiRouteGuard
{
public async ValueTask<bool> CanLeaveAsync(
IUiPageBehavior from,
string toKey,
IUiPageEnterParam? param)
{
// 检查是否有未保存的更改
if (from.Key == "Settings" && HasUnsavedChanges())
{
var confirmed = await ShowConfirmDialog();
return confirmed;
}
- `CanEnterAsync(string uiKey, IUiPageEnterParam? param)`
- `CanLeaveAsync(string uiKey)`
return true;
}
适合放:
public async ValueTask<bool> CanEnterAsync(
string toKey,
IUiPageEnterParam? param)
{
// 进入前的验证
return true;
}
- 未保存设置拦截
- 新手引导期间禁用某些页面跳转
- 多层弹窗切换前的业务确认
private bool HasUnsavedChanges() => true;
private async Task<bool> ShowConfirmDialog() => await Task.FromResult(true);
}
### 过渡处理器
// 注册守卫
uiRouter.AddGuard(new UnsavedChangesGuard());
```
`IUiRouter` 当前公开的是:
### UI 转换处理器
- `RegisterHandler(IUiTransitionHandler handler, UiTransitionHandlerOptions? options = null)`
- `UnregisterHandler(IUiTransitionHandler handler)`
```csharp
using GFramework.Game.Abstractions.UI;
适合放:
public class FadeTransitionHandler : IUiTransitionHandler
{
public async ValueTask OnBeforeEnterAsync(UiTransitionEvent @event)
{
Console.WriteLine($"准备进入 UI: {@event.ToKey}");
await PlayFadeIn();
}
- UI 转场动画
- 统一日志
- 栈变化埋点
public async ValueTask OnAfterEnterAsync(UiTransitionEvent @event)
{
Console.WriteLine($"已进入 UI: {@event.ToKey}");
}
### 输入适配层
public async ValueTask OnBeforeExitAsync(UiTransitionEvent @event)
{
Console.WriteLine($"准备退出 UI: {@event.FromKey}");
await PlayFadeOut();
}
如果项目已经有自己的输入系统,推荐把它适配成:
public async ValueTask OnAfterExitAsync(UiTransitionEvent @event)
{
Console.WriteLine($"已退出 UI: {@event.FromKey}");
}
1. 设备输入 -> `UiInputAction`
2. `IUiRouter.TryDispatchUiAction(...)`
3. 若未被 UI 捕获,再决定是否把输入继续交给 World
private async Task PlayFadeIn() => await Task.Delay(200);
private async Task PlayFadeOut() => await Task.Delay(200);
}
这样可以直接复用当前路由器的动作捕获与阻断语义。
// 注册转换处理器
uiRouter.RegisterHandler(new FadeTransitionHandler());
```
## 与旧写法的边界
### UI 句柄管理
以下说法不再适合作为默认指导:
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
- “所有 UI 都统一通过一个 Show API 管理”
- “UI 系统只有页面栈,不涉及输入阻断和暂停语义”
- “Modal / Topmost 只是视觉层级,不影响交互”
[ContextAware]
public partial class DialogController : IController
{
private UiHandle? _dialogHandle;
当前更准确的理解是:
public void ShowDialog()
{
var uiRouter = this.GetSystem<IUiRouter>();
- 页面栈和层级 UI 是两套入口
- 页面行为不仅有生命周期,还有输入、阻断、暂停契约
- router 是 UI 语义仲裁中心,项目输入层应主动接入它
// 显示对话框并保存句柄
_dialogHandle = uiRouter.Show("ConfirmDialog", UiLayer.Modal);
}
## 推荐阅读
public void CloseDialog()
{
if (_dialogHandle.HasValue)
{
var uiRouter = this.GetSystem<IUiRouter>();
// 使用句柄关闭对话框
uiRouter.Hide(_dialogHandle.Value, UiLayer.Modal, destroy: true);
_dialogHandle = null;
}
}
}
```
### UI 栈管理
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class NavigationController : IController
{
public void ShowUiStack()
{
var uiRouter = this.GetSystem<IUiRouter>();
Console.WriteLine($"UI 栈深度: {uiRouter.Count}");
var current = uiRouter.Peek();
if (current != null)
{
Console.WriteLine($"当前 UI: {current.Key}");
}
}
public bool IsSettingsOpen()
{
var uiRouter = this.GetSystem<IUiRouter>();
return uiRouter.Contains("Settings");
}
public bool IsTopPage(string uiKey)
{
var uiRouter = this.GetSystem<IUiRouter>();
return uiRouter.IsTop(uiKey);
}
}
```
### 多层级 UI 管理
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class LayerController : IController
{
public void ShowMultipleToasts()
{
var uiRouter = this.GetSystem<IUiRouter>();
// Toast 层支持重入,可以同时显示多个
uiRouter.Show("Toast1", UiLayer.Toast);
uiRouter.Show("Toast2", UiLayer.Toast);
uiRouter.Show("Toast3", UiLayer.Toast);
}
public void ClearAllToasts()
{
var uiRouter = this.GetSystem<IUiRouter>();
// 清空 Toast 层的所有 UI
uiRouter.ClearLayer(UiLayer.Toast, destroy: true);
}
public void HideAllDialogs()
{
var uiRouter = this.GetSystem<IUiRouter>();
// 隐藏 Modal 层的所有对话框
uiRouter.HideByKey("ConfirmDialog", UiLayer.Modal, hideAll: true);
}
}
```
## 最佳实践
1. **使用合适的层级**:根据 UI 类型选择正确的层级
```csharp
✓ Page: 主要页面(主菜单、设置、游戏界面)
✓ Overlay: 浮层(信息面板、小窗口)
✓ Modal: 模态对话框(确认框、输入框)
✓ Toast: 轻量提示(消息、通知)
✓ Topmost: 系统级(加载界面、全屏遮罩)
```
2. **使用 Push/Pop 管理临时 UI**:如设置、帮助页面
```csharp
// 打开设置(保留当前页面)
await uiRouter.PushAsync("Settings");
// 关闭设置(返回上一页)
await uiRouter.PopAsync();
```
3. **使用 Replace 切换主要页面**:如从菜单到游戏
```csharp
// 开始游戏(清空 UI 栈)
await uiRouter.ReplaceAsync("Gameplay");
```
4. **在 OnEnter/OnExit 中管理资源**:保持资源管理清晰
```csharp
public void OnEnter(IUiPageEnterParam? param)
{
// 加载资源、绑定事件
BindEvents();
}
public void OnExit()
{
// 清理资源、解绑事件
UnbindEvents();
}
```
5. **使用句柄管理非栈 UI**:对于 Overlay、Modal、Toast 层
```csharp
// 保存句柄
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
// 使用句柄关闭
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
```
6. **避免在 UI 切换时阻塞**:使用异步操作
```csharp
✓ await uiRouter.PushAsync("Settings");
✗ uiRouter.PushAsync("Settings").Wait(); // 可能死锁
```
## 常见问题
### 问题Push、Pop、Replace 有什么区别?
**解答**
- **Push**:压入新 UI暂停当前 UI用于临时页面
- **Pop**:弹出当前 UI恢复上一个 UI用于关闭临时页面
- **Replace**:清空 UI 栈,加载新 UI用于主要页面切换
### 问题:什么时候使用不同的 UI 层级?
**解答**
- **Page**:主要页面,使用栈管理
- **Overlay**:浮层,可叠加显示
- **Modal**:模态对话框,阻挡下层交互
- **Toast**:轻量提示,不阻挡交互
- **Topmost**:系统级,最高优先级
### 问题:如何在 UI 之间传递数据?
**解答**
1. 通过 UI 参数
2. 通过 Model
3. 通过事件
### 问题UI 切换时如何显示过渡动画?
**解答**
使用 UI 转换处理器在 `OnBeforeEnter`/`OnAfterExit` 中播放动画。
### 问题:如何防止用户在 UI 切换时操作?
**解答**
在转换处理器中显示遮罩或禁用输入。
## 相关文档
- [场景系统](/zh-CN/game/scene) - 场景管理
- [Godot UI 系统](/zh-CN/godot/ui) - Godot 引擎集成
- [事件系统](/zh-CN/core/events) - UI 事件通信
- [状态机系统](/zh-CN/core/state-machine) - UI 状态管理
1. [game/index.md](./index.md)
2. [scene.md](./scene.md)
3. [../source-generators/auto-ui-page-generator.md](../source-generators/auto-ui-page-generator.md)
4. `GFramework.Game/README.md`
5. `GFramework.Game.Abstractions/README.md`

View File

@ -1,403 +1,198 @@
---
title: ContextAware 生成器
description: 说明 [ContextAware] 当前会生成什么、何时使用、与 ContextAwareBase 的边界以及测试场景。
---
# ContextAware 生成器
> 自动实现 IContextAware 接口,提供架构上下文访问能力
`[ContextAware]``GFramework.Core.SourceGenerators` 中最常用的一类生成器。它的职责很明确:
## 概述
- 为当前类型自动补齐 `IContextAware`
- 提供可复用的上下文懒加载入口
- 让类型可以直接使用 `this.GetSystem<T>()``this.GetModel<T>()``this.GetUtility<T>()` 等扩展方法
ContextAware 生成器为标记了 `[ContextAware]` 属性的类自动生成 `IContextAware` 接口实现,使类能够便捷地访问架构上下文(
`IArchitectureContext`)。这是 GFramework 中最常用的源码生成器之一,几乎所有需要与架构交互的组件都会使用它。
它不负责注册服务,也不会替你决定应该取哪个 `System` / `Model`。它解决的是“当前类型如何拿到架构上下文”。
### 核心功能
## 当前包关系
- **自动接口实现**:无需手动实现 `IContextAware` 接口的 `SetContext()``GetContext()` 方法
- **懒加载上下文**`Context` 属性在首次访问时自动初始化
- **默认提供者**:使用 `GameContextProvider` 作为默认上下文提供者
- **测试友好**:支持通过 `SetContextProvider()` 配置自定义上下文提供者
- 特性来源:`GFramework.Core.SourceGenerators.Abstractions`
- 生成器实现:`GFramework.Core.SourceGenerators`
- 运行时接口:`GFramework.Core.Abstractions.Rule.IContextAware`
- 常用扩展方法:`GFramework.Core.Extensions`
## 基础使用
如果只安装运行时 `GFramework.Core` 而没有安装 `Core.SourceGenerators``[ContextAware]` 本身不会生效。
### 标记类
使用 `[ContextAware]` 属性标记需要访问架构上下文的类:
## 最小用法
```csharp
using GFramework.Core.SourceGenerators.Abstractions.Rule;
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.Extensions;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class PlayerController : IController
{
public void Initialize()
{
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
var playerModel = this.GetModel<PlayerModel>();
var combatSystem = this.GetSystem<CombatSystem>();
var playerModel = this.GetModel<IPlayerModel>();
var combatSystem = this.GetSystem<ICombatSystem>();
this.SendEvent(new PlayerInitializedEvent());
}
public void Attack(Enemy target)
{
var damage = this.GetUtility<DamageCalculator>().Calculate(this, target);
this.SendCommand(new DealDamageCommand(target, damage));
combatSystem.Bind(playerModel);
}
}
```
### 必要条件
当前最重要的前置条件只有两个:
标记的类必须满足以下条件:
- 必须是 `class`
- 必须声明为 `partial`
1. **必须是 `partial` 类**:生成器需要生成部分类代码
2. **必须是 `class` 类型**:不能是 `struct``interface`
如果缺少这两个条件,生成器不会补代码。
## 当前会生成什么
按当前源码,`[ContextAware]` 会为目标类型生成:
- `IContextAware` 的显式接口实现
- 受保护的 `Context` 属性
- 类型级静态 `SetContextProvider(...)`
- 类型级静态 `ResetContextProvider()`
- 一个实例级 `_context` 缓存字段
- 一个类型级共享的 `_contextProvider`
- 一个类型级锁 `_contextSync`
这意味着它不是“每次访问都现查当前架构”。当前行为更接近:
1. 先看当前实例是否已经缓存了上下文
2. 如果没有,就在同步锁内取共享 provider
3. 若 provider 为空,则回退到 `GameContextProvider`
4. 把拿到的上下文缓存到当前实例
## 当前语义里最容易误解的点
### provider 是按类型共享,不是按实例共享
`SetContextProvider(...)` 影响的是“这个生成类型的后续实例或尚未初始化上下文的实例”,不是全仓库所有 `[ContextAware]`
类型共享同一个 provider。
### provider 切换不会自动刷新已缓存实例
一旦某个实例已经把上下文缓存进 `_context`,后续再调用:
- `SetContextProvider(...)`
- `ResetContextProvider()`
都不会自动改写这个实例的已缓存上下文。
如果你确实要覆盖某个现有实例的上下文,应显式调用:
```csharp
// ✅ 正确
[ContextAware]
public partial class MyController { }
// ❌ 错误:缺少 partial 关键字
[ContextAware]
public class MyController { }
// ❌ 错误:不能用于 struct
[ContextAware]
public partial struct MyStruct { }
((IContextAware)controller).SetContext(context);
```
## 生成的代码
### 生成路径和 `ContextAwareBase` 不是一回事
编译器会为标记的类自动生成以下代码:
当前源码里两者的默认回退策略不同:
- `[ContextAware]` 生成实现
- 通过共享 provider 回退,默认 provider 是 `GameContextProvider`
- 带同步锁,支持 `SetContextProvider(...)` / `ResetContextProvider()`
- `ContextAwareBase`
- 只维护简单的实例级缓存
- 不维护共享 provider
- 默认直接回退到 `GameContext.GetFirstArchitectureContext()`
因此,旧文档里把两条路径混写成“只是写法不同”已经不准确。
## 何时使用 `[ContextAware]`
优先用于这些场景:
- 你的类型不是 `AbstractSystem``AbstractModel``AbstractCommand` 这类已经继承 `ContextAwareBase` 的框架基类
- 你希望在测试中显式切换 provider
- 你需要在同一生成类型上统一切换上下文来源
- 你在 Godot 节点、Controller、ViewModel、包装器类型上只想获得上下文访问能力
典型例子:
- `IController` 实现
- Godot `Node` / `Control` 的项目侧包装器
- 不继承框架基类但要访问架构的辅助类型
## 何时改用 `ContextAwareBase`
以下场景优先考虑 `ContextAwareBase` 或已经继承它的框架基类:
- 你本来就继承 `AbstractSystem``AbstractModel``AbstractCommand``AbstractQuery`
- 你不需要类型级共享 provider
- 你只需要简单的实例级上下文缓存
- 调用线程模型已经天然串行,不需要生成实现那套 provider 切换与同步语义
如果一个类型已经通过继承链拿到了 `ContextAwareBase`,通常没必要再额外标 `[ContextAware]`
## 与 Context Get 注入的关系
`[GetModel]``[GetSystem]``[GetUtility]``[GetService]` 这类字段注入生成器,并不是独立工作的。
按当前 `ContextGetGenerator` 的判定规则,目标类型必须满足以下三者之一:
- 标记了 `[ContextAware]`
- 实现了 `IContextAware`
- 继承了 `ContextAwareBase`
所以更准确的理解是:
- `[ContextAware]` 负责“让类型成为 context-aware 类型”
- Context Get 系列特性负责“在这个前提下继续减少字段取值样板”
## 测试场景
如果测试里不想依赖默认全局上下文,推荐显式配置 provider
```csharp
// <auto-generated/>
#nullable enable
PlayerController.SetContextProvider(new TestContextProvider(testArchitecture.Context));
namespace YourNamespace;
partial class PlayerController : global::GFramework.Core.Abstractions.Rule.IContextAware
try
{
private global::GFramework.Core.Abstractions.Architecture.IArchitectureContext? _context;
private static global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider? _contextProvider;
/// <summary>
/// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider
/// </summary>
protected global::GFramework.Core.Abstractions.Architecture.IArchitectureContext Context
{
get
{
if (_context == null)
{
_contextProvider ??= new global::GFramework.Core.Architecture.GameContextProvider();
_context = _contextProvider.GetContext();
}
return _context;
}
}
/// <summary>
/// 配置上下文提供者(用于测试或多架构场景)
/// </summary>
/// <param name="provider">上下文提供者实例</param>
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider provider)
{
_contextProvider = provider;
}
/// <summary>
/// 重置上下文提供者为默认值(用于测试清理)
/// </summary>
public static void ResetContextProvider()
{
_contextProvider = null;
}
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architecture.IArchitectureContext context)
{
_context = context;
}
global::GFramework.Core.Abstractions.Architecture.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
{
return Context;
}
var controller = new PlayerController();
controller.Initialize();
}
```
### 代码解析
生成的代码包含以下关键部分:
1. **私有字段**
- `_context`:缓存的上下文实例
- `_contextProvider`:静态上下文提供者(所有实例共享)
2. **Context 属性**
- `protected` 访问级别,子类可访问
- 懒加载:首次访问时自动初始化
- 使用 `GameContextProvider` 作为默认提供者
3. **配置方法**
- `SetContextProvider()`:设置自定义上下文提供者
- `ResetContextProvider()`:重置为默认提供者
4. **显式接口实现**
- `IContextAware.SetContext()`:允许外部设置上下文
- `IContextAware.GetContext()`:返回当前上下文
## 配置上下文提供者
### 测试场景
在单元测试中,通常需要使用自定义的上下文提供者:
```csharp
[Test]
public async Task TestPlayerController()
finally
{
// 创建测试架构
var testArchitecture = new TestArchitecture();
await testArchitecture.InitAsync();
// 配置自定义上下文提供者
PlayerController.SetContextProvider(new TestContextProvider(testArchitecture));
try
{
// 测试代码
var controller = new PlayerController();
controller.Initialize();
// 验证...
}
finally
{
// 清理:重置上下文提供者
PlayerController.ResetContextProvider();
}
}
```
### 多架构场景
在某些高级场景中,可能需要同时运行多个架构实例:
```csharp
public class MultiArchitectureManager
{
private readonly Dictionary<string, IArchitecture> _architectures = new();
public void SwitchToArchitecture(string name)
{
var architecture = _architectures[name];
var provider = new ScopedContextProvider(architecture);
// 为所有使用 [ContextAware] 的类切换上下文
PlayerController.SetContextProvider(provider);
EnemyController.SetContextProvider(provider);
// ...
}
}
```
## 使用场景
### 何时使用 [ContextAware]
推荐在以下场景使用 `[ContextAware]` 属性:
1. **Controller 层**:需要协调多个 Model/System 的控制器
2. **Command/Query 实现**:需要访问架构服务的命令或查询
3. **自定义组件**:不继承框架基类但需要上下文访问的组件
```csharp
[ContextAware]
public partial class GameFlowController : IController
{
public async Task StartGame()
{
var saveSystem = this.GetSystem<SaveSystem>();
var uiSystem = this.GetSystem<UISystem>();
await saveSystem.LoadAsync();
await uiSystem.ShowMainMenuAsync();
}
}
```
### 与 IController 配合使用
在 Godot 项目中,控制器通常同时实现 `IController` 和使用 `[ContextAware]`
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class PlayerController : Node, IController
{
public override void _Ready()
{
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
var playerModel = this.GetModel<PlayerModel>();
var combatSystem = this.GetSystem<CombatSystem>();
}
}
```
**说明**
- `IController` 是标记接口,标识这是一个控制器
- `[ContextAware]` 提供架构访问能力
- 两者配合使用是推荐的模式
### 何时继承 ContextAwareBase
如果类需要更多框架功能(如生命周期管理),应继承 `ContextAwareBase`
```csharp
// 推荐:需要生命周期管理时继承基类
public class PlayerModel : AbstractModel
{
// AbstractModel 已经继承了 ContextAwareBase
protected override void OnInit()
{
var config = this.GetUtility<ConfigLoader>().Load<PlayerConfig>();
}
}
// 推荐:简单组件使用属性
[ContextAware]
public partial class SimpleHelper
{
public void DoSomething()
{
this.SendEvent(new SomethingHappenedEvent());
}
}
```
## 与 IContextAware 接口的关系
生成的代码实现了 `IContextAware` 接口:
```csharp
namespace GFramework.Core.Abstractions.Rule;
public interface IContextAware
{
void SetContext(IArchitectureContext context);
IArchitectureContext GetContext();
}
```
这意味着标记了 `[ContextAware]` 的类可以:
1. **被架构自动注入上下文**:实现 `IContextAware` 的类在注册到架构时会自动调用 `SetContext()`
2. **参与依赖注入**:可以作为 `IContextAware` 类型注入到其他组件
3. **支持上下文传递**:可以通过 `GetContext()` 将上下文传递给其他组件
## 最佳实践
### 1. 始终使用 partial 关键字
```csharp
// ✅ 正确
[ContextAware]
public partial class MyController { }
// ❌ 错误:编译器会报错
[ContextAware]
public class MyController { }
```
### 2. 在测试中清理上下文提供者
```csharp
[TearDown]
public void TearDown()
{
// 避免测试之间的状态污染
PlayerController.ResetContextProvider();
EnemyController.ResetContextProvider();
}
```
### 3. 避免在构造函数中访问 Context
需要注意两点:
```csharp
[ContextAware]
public partial class MyController
{
// ❌ 错误:构造函数执行时上下文可能未初始化
public MyController()
{
var model = this.GetModel<SomeModel>(); // 可能为 null
}
- `ResetContextProvider()` 只会重置共享 provider不会清除已创建实例上的 `_context`
- 如果测试要复用同一实例并切换上下文,应该显式调用 `((IContextAware)instance).SetContext(...)`
// ✅ 正确:在初始化方法中访问
public void Initialize()
{
var model = this.GetModel<SomeModel>(); // 安全
}
}
```
## 诊断与约束
### 4. 优先使用 Context 属性而非接口方法
当前文档里最值得记住的约束只有这些:
```csharp
[ContextAware]
public partial class MyController
{
public void DoSomething()
{
// ✅ 推荐:使用扩展方法
var model = this.GetModel<SomeModel>();
- 非 `class` 会触发 `GF_Rule_001`
- 非 `partial` 不会生成实现,并会触发公共 partial 约束诊断
- 嵌套、字段注入等其他错误通常由对应的 Context Get 生成器和其诊断补充报告
// ❌ 不推荐:显式调用接口方法
var context = ((IContextAware)this).GetContext();
var model2 = context.GetModel<SomeModel>();
}
}
```
## 与旧写法的边界
## 诊断信息
下面这些旧说法已经不够准确:
生成器会在以下情况报告编译错误:
- “`[ContextAware]` 只是帮你补一个简单的 `GetContext()`
- “切换 provider 后,已有实例会自动跟着切换”
- “`[ContextAware]``ContextAwareBase` 的默认行为完全一致”
### GFSG001: 类必须是 partial
当前更准确的理解是:
```csharp
[ContextAware]
public class MyController { } // 错误:缺少 partial 关键字
```
- 生成实现带有实例缓存、类型级共享 provider 和同步锁
- provider 切换只影响尚未缓存上下文的实例
- `ContextAwareBase` 是更轻量的实例级缓存路径
**解决方案**:添加 `partial` 关键字
## 推荐阅读
```csharp
[ContextAware]
public partial class MyController { } // ✅ 正确
```
### GFSG002: ContextAware 只能用于类
```csharp
[ContextAware]
public partial struct MyStruct { } // 错误:不能用于 struct
```
**解决方案**:将 `struct` 改为 `class`
```csharp
[ContextAware]
public partial class MyClass { } // ✅ 正确
```
## 相关文档
- [Source Generators 概述](./index)
- [架构上下文](../core/context)
- [IContextAware 接口](../core/rule)
- [日志生成器](./logging-generator)
1. [context-get-generator.md](./context-get-generator.md)
2. [logging-generator.md](./logging-generator.md)
3. [../core/index.md](../core/index.md)
4. `GFramework.Core.SourceGenerators/README.md`

View File

@ -1,250 +1,104 @@
---
title: Priority 生成器
description: 说明 [Priority] 当前会生成什么、何时生效、应配合哪些优先级 API 使用,以及动态优先级的边界。
---
# Priority 生成器
> 自动实现 IPrioritized 接口,为类添加优先级标记
`[Priority]` 的职责很简单:为目标类型自动生成 `IPrioritized.Priority`
Priority 生成器通过源代码生成器自动实现 `IPrioritized` 接口,简化优先级标记和排序逻辑的实现。
它本身不是调度器,也不会自动改变系统、服务或处理器的执行顺序。只有调用方使用了“按优先级排序”的检索入口,生成出来的
`Priority` 才会真正影响顺序。
## 概述
## 当前包关系
### 核心功能
- 特性来源:`GFramework.Core.SourceGenerators.Abstractions`
- 生成器实现:`GFramework.Core.SourceGenerators`
- 运行时契约:`GFramework.Core.Abstractions.Bases.IPrioritized`
- 预定义常量:`GFramework.Core.Abstractions.Bases.PriorityGroup`
- **自动实现接口**:自动实现 `IPrioritized` 接口的 `Priority` 属性
- **优先级标记**:通过特性参数指定优先级值
- **编译时生成**:在编译时生成代码,零运行时开销
- **类型安全**:编译时类型检查,避免运行时错误
### 适用场景
- 系统初始化顺序控制
- 事件处理器优先级排序
- 服务注册顺序管理
- 需要按优先级排序的任何场景
## 基础使用
### 标记优先级
使用 `[Priority]` 特性为类标记优先级:
```csharp
using GFramework.Core.SourceGenerators.Abstractions.Bases;
[Priority(10)]
public partial class MySystem
{
// 自动生成 IPrioritized 接口实现
}
```
### 生成代码
编译器会自动生成如下代码:
```csharp
// <auto-generated/>
#nullable enable
namespace YourNamespace;
partial class MySystem : global::GFramework.Core.Abstractions.Bases.IPrioritized
{
/// <summary>
/// 获取优先级值: 10
/// </summary>
public int Priority => 10;
}
```
### 使用生成的优先级
生成的 `Priority` 属性可用于排序:
```csharp
using GFramework.Core.Abstractions.Bases;
// 获取所有实现了 IPrioritized 的系统
var systems = new List<IPrioritized> { system1, system2, system3 };
// 按优先级排序(值越小,优先级越高)
var sorted = systems.OrderBy(s => s.Priority).ToList();
// 依次初始化
foreach (var system in sorted)
{
system.Initialize();
}
```
## 优先级值语义
### 值的含义
| 优先级值范围 | 含义 | 使用场景 |
|--------|-------|------------------|
| 负数 | 高优先级 | 核心系统、关键事件处理器 |
| 0 | 默认优先级 | 普通系统、一般事件处理器 |
| 正数 | 低优先级 | 可延迟初始化的系统、非关键处理器 |
### PriorityGroup 常量
推荐使用 `PriorityGroup` 预定义常量来标记优先级:
## 最小用法
```csharp
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.SourceGenerators.Abstractions.Bases;
[Priority(PriorityGroup.Critical)] // -100
public partial class InputSystem : AbstractSystem { }
[Priority(PriorityGroup.High)] // -50
public partial class PhysicsSystem : AbstractSystem { }
[Priority(PriorityGroup.Normal)] // 0
public partial class GameplaySystem : AbstractSystem { }
[Priority(PriorityGroup.Low)] // 50
public partial class AudioSystem : AbstractSystem { }
[Priority(PriorityGroup.Deferred)] // 100
public partial class CleanupSystem : AbstractSystem { }
```
**PriorityGroup 常量定义**
```csharp
namespace GFramework.Core.Abstractions.Bases;
public static class PriorityGroup
[Priority(PriorityGroup.High)]
public partial class SaveSystem : AbstractSystem
{
public const int Critical = -100; // 关键:输入、网络等
public const int High = -50; // 高:物理、碰撞等
public const int Normal = 0; // 正常:游戏逻辑等
public const int Low = 50; // 低:音频、特效等
public const int Deferred = 100; // 延迟:清理、统计等
protected override void OnInit()
{
}
}
```
## 使用场景
### 系统初始化顺序
控制系统初始化的顺序,确保依赖关系正确:
当前生成器会补出:
```csharp
public int Priority => PriorityGroup.High;
```
优先级值越小,优先级越高。
## 当前真正会读取优先级的入口
### `IIocContainer`
如果你直接在容器层取集合,使用:
```csharp
var handlers = container.GetAllByPriority<IMyHandler>();
```
### `IArchitectureContext`
当前推荐按组件类别使用这些 API
- `GetServicesByPriority<TService>()`
- `GetSystemsByPriority<TSystem>()`
- `GetModelsByPriority<TModel>()`
- `GetUtilitiesByPriority<TUtility>()`
### `IContextAware` 扩展方法
如果你已经在 `[ContextAware]` 类型或 `ContextAwareBase` 派生类型里,直接用:
- `this.GetServicesByPriority<TService>()`
- `this.GetSystemsByPriority<TSystem>()`
- `this.GetModelsByPriority<TModel>()`
- `this.GetUtilitiesByPriority<TUtility>()`
这比旧文档里反复出现的 `this.GetAllByPriority<T>()` 更贴近当前公开扩展方法。
## 最小接入示例
### 系统排序
```csharp
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Extensions;
using GFramework.Core.SourceGenerators.Abstractions.Bases;
using GFramework.Core.Abstractions.Bases;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
// 输入系统最先初始化
[Priority(PriorityGroup.Critical)]
public partial class InputSystem : AbstractSystem
{
protected override void OnInit()
{
// 初始化输入处理
}
}
// 物理系统次之
[Priority(PriorityGroup.High)]
public partial class PhysicsSystem : AbstractSystem
[ContextAware]
public partial class SystemBootstrapper : IController
{
protected override void OnInit()
public void Start()
{
// 初始化物理引擎
}
}
// 游戏逻辑系统在中间
[Priority(PriorityGroup.Normal)]
public partial class GameplaySystem : AbstractSystem
{
protected override void OnInit()
{
// 初始化游戏逻辑
}
}
// 音频系统可以稍后
[Priority(PriorityGroup.Low)]
public partial class AudioSystem : AbstractSystem
{
protected override void OnInit()
{
// 初始化音频引擎
}
}
```
在架构中按优先级初始化:
```csharp
public class GameArchitecture : Architecture
{
protected override void InitSystems()
{
// 获取所有系统并按优先级排序
var systems = this.GetAllByPriority<ISystem>();
var systems = this.GetSystemsByPriority<ISystem>();
foreach (var system in systems)
{
system.Init();
}
}
}
```
### 事件处理器优先级
控制事件处理器的执行顺序:
```csharp
using GFramework.Core.Abstractions.Events;
// 关键事件处理器最先执行
[Priority(PriorityGroup.Critical)]
public partial class CriticalEventHandler : IEventHandler<CriticalEvent>
{
public void Handle(CriticalEvent e)
{
// 处理关键事件
}
}
// 普通处理器在中间执行
[Priority(PriorityGroup.Normal)]
public partial class NormalEventHandler : IEventHandler<CriticalEvent>
{
public void Handle(CriticalEvent e)
{
// 处理普通逻辑
}
}
// 日志记录器最后执行
[Priority(PriorityGroup.Deferred)]
public partial class EventLogger : IEventHandler<CriticalEvent>
{
public void Handle(CriticalEvent e)
{
// 记录日志
}
}
```
事件总线按优先级调用处理器:
```csharp
public class EventBus : IEventBus
{
public void Send<TEvent>(TEvent e) where TEvent : IEvent
{
// 获取所有处理器并按优先级排序
var handlers = this.GetAllByPriority<IEventHandler<TEvent>>();
foreach (var handler in handlers)
{
handler.Handle(e);
system.Initialize();
}
}
}
@ -252,496 +106,111 @@ public class EventBus : IEventBus
### 服务排序
控制多个服务实现的优先级:
```csharp
// 高优先级服务
[Priority(PriorityGroup.High)]
public partial class PremiumService : IService
public partial class PremiumSaveMigration : ISaveMigration
{
public void Execute()
{
// 优先执行
}
}
// 默认服务
[Priority(PriorityGroup.Normal)]
public partial class DefaultService : IService
{
public void Execute()
{
// 默认执行
}
}
// 后备服务
[Priority(PriorityGroup.Low)]
public partial class FallbackService : IService
public partial class MetricsSaveMigration : ISaveMigration
{
public void Execute()
}
var migrations = architecture.Context.GetServicesByPriority<ISaveMigration>();
```
## `PriorityGroup` 的角色
当前仓库提供了这些预定义常量:
- `PriorityGroup.Critical`
- `PriorityGroup.High`
- `PriorityGroup.Normal`
- `PriorityGroup.Low`
- `PriorityGroup.Deferred`
文档不应该把这些值解释成硬编码的生命周期阶段。它们只是团队共享的排序语义常量,具体“高优先级意味着先做什么”仍然取决于
调用方对排序结果的使用方式。
如果项目有更细粒度的排序约定,也可以直接传 `int`,或在项目层自定义自己的优先级常量。
## 何时使用 `[Priority]`
适合以下场景:
- 类型顺序在编译期就能确定
- 你不想手写 `IPrioritized`
- 同一类型的所有实例都应共享同一个优先级
常见例子:
- 初始化顺序明确的系统
- 顺序敏感的服务实现
- 有先后要求的处理器或迁移器
## 何时不要使用 `[Priority]`
以下场景应改为手写 `IPrioritized`
- 优先级要依赖运行时配置
- 优先级要根据环境、开关或状态动态变化
- 你已经手动实现了 `IPrioritized`
例如:
```csharp
public sealed class DynamicPrioritySystem : IPrioritized
{
private readonly bool _enabled;
public DynamicPrioritySystem(bool enabled)
{
// 最后备选
_enabled = enabled;
}
public int Priority => _enabled ? PriorityGroup.High : PriorityGroup.Deferred;
}
```
## 与 PriorityUsageAnalyzer 集成
## 当前诊断与约束
### GF_Priority_Usage_001 诊断
`[Priority]` 当前有几条直接约束:
`PriorityUsageAnalyzer` 分析器会检测应该使用 `GetAllByPriority<T>()` 而非 `GetAll<T>()` 的场景:
- `GF_Priority_001`
- 只能标在 `class`
- `GF_Priority_002`
- 目标类型已经手写实现 `IPrioritized`
- `GF_Priority_003`
- 类型必须是 `partial`
- `GF_Priority_004`
- 特性值缺失或无效
- `GF_Priority_005`
- 不支持嵌套类
**错误示例**
对文档而言,最关键的结论是
```csharp
// ❌ 不推荐:可能未按优先级排序
var systems = context.GetAll<ISystem>();
```
- `partial` 是强约束
- 顶层类是强约束
- 手写实现与生成实现只能二选一
**正确示例**
## 与旧写法的边界
```csharp
// ✅ 推荐:确保按优先级排序
var systems = context.GetAllByPriority<ISystem>();
```
下面这些旧写法或旧表述已经不再适合作为默认指导:
### 分析器规则
- 在 `IContextAware` 类型里统一写 `this.GetAllByPriority<T>()`
- 继续用 `system.Init()` 作为系统初始化示例
- 把 `[Priority]` 写成“标了就会自动改变执行顺序”
当满足以下条件时,分析器会报告 `GF_Priority_Usage_001` 诊断:
前更准确的理解是
1. 类型实现了 `IPrioritized` 接口
2. 使用了 `GetAll<T>()` 方法
3. 建议改用 `GetAllByPriority<T>()` 方法
- `[Priority]` 只生成 `Priority`
- 排序效果依赖容器、上下文或扩展方法是否走了 priority-aware API
- `IContextAware` 路径更推荐按组件类别使用 `GetSystemsByPriority` / `GetServicesByPriority` 等入口
## 诊断信息
## 推荐阅读
### GF_Priority_001 - 只能应用于类
**错误信息**`Priority attribute can only be applied to classes`
**场景**:将 `[Priority]` 特性应用于非类类型
```csharp
[Priority(10)]
public interface IMyInterface // ❌ 错误
{
}
[Priority(10)]
public struct MyStruct // ❌ 错误
{
}
```
**解决方案**:只在类上使用 `[Priority]` 特性
```csharp
[Priority(10)]
public partial class MyClass // ✅ 正确
{
}
```
### GF_Priority_002 - 已实现 IPrioritized 接口
**错误信息**`Type '{ClassName}' already implements IPrioritized interface`
**场景**:类已手动实现 `IPrioritized` 接口
```csharp
[Priority(10)]
public partial class MySystem : IPrioritized // ❌ 冲突
{
public int Priority => 10; // 手动实现
}
```
**解决方案**:移除 `[Priority]` 特性或移除手动实现
```csharp
// 方案1移除特性使用手动实现
public partial class MySystem : IPrioritized
{
public int Priority => 10;
}
// 方案2移除手动实现使用生成器
[Priority(10)]
public partial class MySystem
{
// 生成器自动实现
}
```
### GF_Priority_003 - 必须声明为 partial
**错误信息**`Class '{ClassName}' must be declared as partial`
**场景**:类未声明为 `partial`
```csharp
[Priority(10)]
public class MySystem // ❌ 缺少 partial
{
}
```
**解决方案**:添加 `partial` 关键字
```csharp
[Priority(10)]
public partial class MySystem // ✅ 正确
{
}
```
### GF_Priority_004 - 优先级值无效
**错误信息**`Priority value is invalid`
**场景**:特性参数无效或未提供
```csharp
[Priority] // ❌ 缺少参数
public partial class MySystem
{
}
```
**解决方案**:提供有效的优先级值
```csharp
[Priority(10)] // ✅ 正确
public partial class MySystem
{
}
```
### GF_Priority_005 - 不支持嵌套类
**错误信息**`Nested class '{ClassName}' is not supported`
**场景**:在嵌套类中使用 `[Priority]` 特性
```csharp
public partial class OuterClass
{
[Priority(10)]
public partial class InnerClass // ❌ 错误
{
}
}
```
**解决方案**:将嵌套类提取为独立的类
```csharp
[Priority(10)]
public partial class InnerClass // ✅ 正确
{
}
```
## 最佳实践
### 1. 使用 PriorityGroup 常量
避免使用魔法数字,优先使用预定义常量:
```csharp
// ✅ 推荐:使用常量
[Priority(PriorityGroup.Critical)]
public partial class InputSystem { }
// ❌ 不推荐:魔法数字
[Priority(-100)]
public partial class InputSystem { }
```
### 2. 预留优先级间隔
为未来扩展预留间隔:
```csharp
public static class SystemPriority
{
public const int Input = -100;
public const int PrePhysics = -90; // 预留扩展
public const int Physics = -80;
public const int PostPhysics = -70; // 预留扩展
public const int Gameplay = 0;
public const int PostGameplay = 10; // 预留扩展
public const int Audio = 50;
public const int Cleanup = 100;
}
```
### 3. 文档化优先级语义
为自定义优先级值添加注释:
```csharp
/// <summary>
/// 输入系统,优先级 -100需要最先初始化以接收输入事件
/// </summary>
[Priority(PriorityGroup.Critical)]
public partial class InputSystem : AbstractSystem
{
}
```
### 4. 避免优先级冲突
当多个类有相同优先级时,执行顺序不确定。应避免依赖特定顺序:
```csharp
// ❌ 不推荐:相同优先级,顺序不确定
[Priority(0)]
public partial class SystemA { }
[Priority(0)]
public partial class SystemB { }
// ✅ 推荐:明确区分优先级
[Priority(-10)]
public partial class SystemA { }
[Priority(0)]
public partial class SystemB { }
```
### 5. 只在真正需要排序的场景使用
不要滥用优先级特性,只在确实需要排序的场景使用:
```csharp
// ✅ 推荐:需要排序的系统
[Priority(PriorityGroup.Critical)]
public partial class InputSystem : AbstractSystem { }
// ❌ 不推荐:不需要排序的工具类
[Priority(10)]
public static partial class MathHelper // 静态工具类无需优先级
{
public static int Add(int a, int b) => a + b;
}
```
## 高级场景
### 泛型类支持
Priority 特性支持泛型类:
```csharp
[Priority(20)]
public partial class GenericSystem<T> : ISystem
{
public void Init()
{
// 泛型系统的初始化
}
}
```
### 与其他特性组合
Priority 可以与其他源代码生成器特性组合使用:
```csharp
using GFramework.Core.SourceGenerators.Abstractions.Bases;
using GFramework.Core.SourceGenerators.Abstractions.Logging;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[Priority(PriorityGroup.High)]
[Log]
[ContextAware]
public partial class HighPrioritySystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("High priority system initialized");
}
}
```
### 运行时优先级查询
可以在运行时查询优先级值:
```csharp
public void ProcessSystems()
{
var systems = this.GetAllByPriority<ISystem>();
foreach (var system in systems)
{
if (system is IPrioritized prioritized)
{
Console.WriteLine($"System: {system.GetType().Name}, Priority: {prioritized.Priority}");
}
system.Init();
}
}
```
## 常见问题
### Q: 优先级值可以是任意整数吗?
**A**: 是的,优先级值可以是任何 `int` 类型的值。推荐使用 `PriorityGroup` 预定义常量或在项目中定义自己的优先级常量。
### Q: 多个类有相同优先级会怎样?
**A**: 当多个类有相同的优先级值时,它们的相对顺序是不确定的。建议为每个类设置不同的优先级值,或在文档中明确说明相同优先级类的执行顺序不保证。
### Q: 可以在运行时改变优先级吗?
**A**: 不可以。`Priority` 属性是只读的,值在编译时确定。如果需要运行时改变优先级,应手动实现 `IPrioritized` 接口。
```csharp
public class DynamicPrioritySystem : IPrioritized
{
private int _priority;
public int Priority => _priority;
public void SetPriority(int value)
{
_priority = value;
}
}
```
### Q: 优先级支持继承吗?
**A**: `[Priority]` 特性标记为 `Inherited = false`,不会被子类继承。每个子类需要独立标记优先级。
```csharp
[Priority(10)]
public partial class BaseSystem { }
public partial class DerivedSystem : BaseSystem // 不会继承 Priority = 10
{
// 需要重新标记
// [Priority(20)]
}
```
### Q: 如何在不使用特性的情况下实现优先级?
**A**: 可以手动实现 `IPrioritized` 接口:
```csharp
public class ManualPrioritySystem : IPrioritized
{
public int Priority => 10;
// 手动实现提供更大的灵活性
public int Priority => _config.Enabled ? 10 : 100;
}
```
### Q: 负数优先级和正数优先级的区别是什么?
**A**: 优先级值用于排序,通常值越小优先级越高:
- 负数(-100高优先级最先执行
- 零0默认优先级
- 正数100低优先级最后执行
具体含义取决于使用场景,但推荐遵循 `PriorityGroup` 定义的语义。
## 实际应用示例
### 游戏系统架构
完整的游戏系统初始化示例:
```csharp
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.SourceGenerators.Abstractions.Bases;
using GFramework.Core.Abstractions.Bases;
// 输入系统(最先初始化)
[Priority(PriorityGroup.Critical)]
[Log]
public partial class InputSystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("Input system initialized");
}
}
// 物理系统
[Priority(PriorityGroup.High)]
[Log]
public partial class PhysicsSystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("Physics system initialized");
}
}
// 游戏逻辑系统
[Priority(PriorityGroup.Normal)]
[Log]
[ContextAware]
public partial class GameplaySystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("Gameplay system initialized");
}
}
// 音频系统
[Priority(PriorityGroup.Low)]
[Log]
public partial class AudioSystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("Audio system initialized");
}
}
// 清理系统(最后执行)
[Priority(PriorityGroup.Deferred)]
[Log]
public partial class CleanupSystem : AbstractSystem
{
protected override void OnInit()
{
Logger.Info("Cleanup system initialized");
}
}
```
在架构中初始化:
```csharp
public class GameArchitecture : Architecture
{
protected override async Task InitAsync()
{
// 获取所有系统并按优先级排序
var systems = this.GetAllByPriority<ISystem>();
foreach (var system in systems)
{
await system.InitAsync();
}
}
}
```
## 相关文档
- [Source Generators 概述](./index.md)
- [ContextAware 生成器](./context-aware-generator.md)
- [系统初始化](../core/system.md)
1. [context-aware-generator.md](./context-aware-generator.md)
2. [context-get-generator.md](./context-get-generator.md)
3. [../core/index.md](../core/index.md)
4. `GFramework.Core.SourceGenerators/README.md`