mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-14 06:34:30 +08:00
Compare commits
22 Commits
60faf8eaff
...
d836ec8027
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d836ec8027 | ||
|
|
48e45787f3 | ||
|
|
da707c7b4f | ||
|
|
9ccfed3ad9 | ||
|
|
4d306498b9 | ||
|
|
4a779ac794 | ||
|
|
a5a35ce6ed | ||
|
|
240fc761ed | ||
|
|
aa78dfbf51 | ||
|
|
c61ee140a1 | ||
|
|
2c678cbdda | ||
|
|
233195df91 | ||
|
|
33c435bad5 | ||
|
|
26d5d84d26 | ||
|
|
035c7db18e | ||
|
|
f044aeb770 | ||
|
|
ec0c9a7bc8 | ||
|
|
b553d7cbc6 | ||
|
|
ff1996e81b | ||
|
|
358b1e9cca | ||
|
|
462a71ba3c | ||
|
|
5c7870ca3e |
@ -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
|
||||
|
||||
|
||||
@ -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"]
|
||||
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", [])
|
||||
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: {check['explanation']}")
|
||||
lines.append(f" Resolution: {check['resolution']}")
|
||||
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)
|
||||
if "actionable" in selected_sections:
|
||||
lines.append("")
|
||||
lines.append(f"CodeRabbit actionable comments: {actionable_count}")
|
||||
for comment in comments:
|
||||
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: {comment['title']}")
|
||||
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
|
||||
if comment["description"]:
|
||||
lines.append(f" Description: {comment['description']}")
|
||||
if actionable_count and not comments:
|
||||
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)
|
||||
if "outside-diff" in selected_sections:
|
||||
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"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: {comment['title']}")
|
||||
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
|
||||
if comment["description"]:
|
||||
lines.append(f" Description: {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)
|
||||
if "nitpick" in selected_sections:
|
||||
lines.append("")
|
||||
lines.append(f"CodeRabbit nitpick comments: {nitpick_count} declared, {len(nitpick_comments)} parsed")
|
||||
for comment in nitpick_comments:
|
||||
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: {comment['title']}")
|
||||
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
|
||||
if comment["description"]:
|
||||
lines.append(f" Description: {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,8 +925,9 @@ 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)}")
|
||||
|
||||
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):
|
||||
@ -801,15 +943,19 @@ def format_text(result: dict[str, Any]) -> str:
|
||||
|
||||
if report["has_failed_tests"]:
|
||||
for failed_test in report["failed_tests"]:
|
||||
lines.append(f" Failed test: {failed_test}")
|
||||
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__":
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -21,3 +23,54 @@ 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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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,25 +115,25 @@ 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;
|
||||
@ -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
|
||||
var destroySnapshot = TryBeginDestroy();
|
||||
if (destroySnapshot == null)
|
||||
{
|
||||
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)
|
||||
{
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
// 释放锁资源
|
||||
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 result = TryPopEntry(token);
|
||||
if (result.ShouldNotify)
|
||||
{
|
||||
var current = stack.Pop();
|
||||
if (current.TokenId == token.Id)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
NotifyHandlers(result.NotifyGroup, false);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
NotifyHandlers(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);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using GFramework.Core.Abstractions.Query;
|
||||
using GFramework.Core.Rule;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs.Query;
|
||||
|
||||
namespace GFramework.Core.Query;
|
||||
|
||||
@ -25,3 +26,30 @@ public abstract class AbstractAsyncQuery<TResult> : ContextAwareBase, IAsyncQuer
|
||||
/// <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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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>
|
||||
@ -26,3 +27,30 @@ public abstract class AbstractQuery<TResult> : ContextAwareBase, IQuery<TResult>
|
||||
/// <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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
var context = EnterDispatchScope(
|
||||
action,
|
||||
out var middlewaresSnapshot,
|
||||
out var reducersSnapshot,
|
||||
out var stateComparerSnapshot);
|
||||
|
||||
enteredDispatchScope = true;
|
||||
context = new StoreDispatchContext<TState>(action!, _state);
|
||||
stateComparerSnapshot = _stateComparer;
|
||||
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
|
||||
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
|
||||
}
|
||||
|
||||
// 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>
|
||||
@ -948,6 +1013,19 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
||||
"Caller must hold _lock before invoking CreateReducerSnapshotCore to avoid concurrency bugs.");
|
||||
|
||||
if (_actionMatchingMode == StoreActionMatchingMode.ExactTypeOnly)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -963,6 +1041,38 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
||||
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,12 +1094,16 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
||||
}
|
||||
}
|
||||
|
||||
if (matches is null || matches.Count == 0)
|
||||
{
|
||||
return Array.Empty<IStoreReducerAdapter>();
|
||||
return matches;
|
||||
}
|
||||
|
||||
matches.Sort(static (left, right) =>
|
||||
/// <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)
|
||||
@ -1004,15 +1118,6 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
||||
}
|
||||
|
||||
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 snapshot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -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 (!activationMetadata.ImplementsRegistryContract)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
|
||||
if (!TryCreateGeneratedRegistries(registryTypes, assemblyName, logger, out var registries))
|
||||
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)
|
||||
{
|
||||
@ -192,6 +144,132 @@ internal static class CqrsHandlerRegistrar
|
||||
}
|
||||
}
|
||||
|
||||
/// <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,12 +374,47 @@ 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))
|
||||
{
|
||||
TryAppendNamedFallbackType(assembly, resolvedTypes, assemblyName, typeName, logger);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
@ -287,7 +423,7 @@ internal static class CqrsHandlerRegistrar
|
||||
{
|
||||
logger.Warn(
|
||||
$"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry.");
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
resolvedTypes.Add(type);
|
||||
@ -299,13 +435,6 @@ internal static class CqrsHandlerRegistrar
|
||||
}
|
||||
}
|
||||
|
||||
return new ReflectionFallbackMetadata(
|
||||
resolvedTypes
|
||||
.Distinct()
|
||||
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安全获取程序集中的可加载类型,并在部分类型加载失败时保留其余处理器注册能力。
|
||||
/// </summary>
|
||||
|
||||
@ -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/`
|
||||
|
||||
@ -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-up(RP-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=`,避免把环境问题误判成代码回归
|
||||
|
||||
@ -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/`,避免默认启动入口再次膨胀
|
||||
|
||||
@ -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,避免默认入口继续膨胀
|
||||
|
||||
@ -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
|
||||
eventBus.Send(new PlayerJoinedEvent("Alice"));
|
||||
```
|
||||
|
||||
如果你还需要统计、过滤或弱引用订阅,可以改用 `EnhancedEventBus`。它在 `EventBus` 基础上额外提供:
|
||||
|
||||
- `Statistics`
|
||||
- `SendFilterable(...)` / `RegisterFilterable(...)`
|
||||
- `SendWeak(...)` / `RegisterWeak(...)`
|
||||
|
||||
这类能力更适合工具层、编辑器层或长生命周期对象,不必默认扩散到每个业务事件。
|
||||
|
||||
## 优先级、传播与上下文事件
|
||||
|
||||
当事件处理顺序或“是否继续传播”本身就是语义的一部分时,使用优先级入口:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Events;
|
||||
using GFramework.Core.Events;
|
||||
|
||||
public sealed record InputCommand(string Name);
|
||||
|
||||
var eventBus = new EventBus();
|
||||
|
||||
eventBus.RegisterWithContext<InputCommand>(ctx =>
|
||||
{
|
||||
Position = new Vector3(10, 0, 5)
|
||||
});
|
||||
|
||||
// 发送事件(自动创建实例)
|
||||
eventBus.Send<PlayerDiedEvent>();
|
||||
|
||||
// 注销事件监听器
|
||||
eventBus.UnRegister<PlayerDiedEvent>(OnPlayerDied);
|
||||
```
|
||||
|
||||
### DefaultUnRegister
|
||||
|
||||
默认注销器实现,封装注销回调。
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
Action onUnregister = () => Console.WriteLine("Unregistered");
|
||||
var unregister = new DefaultUnRegister(onUnregister);
|
||||
|
||||
// 执行注销
|
||||
unregister.UnRegister();
|
||||
```
|
||||
|
||||
### OrEvent
|
||||
|
||||
事件或运算组合器,当任意一个事件触发时触发。
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
OrEvent Or(IEvent @event); // 添加要组合的事件
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
var onAnyInput = new OrEvent()
|
||||
.Or(onKeyPressed)
|
||||
.Or(onMouseClicked)
|
||||
.Or(onTouchDetected);
|
||||
|
||||
// 当上述任意事件触发时,执行回调
|
||||
onAnyInput.Register(() =>
|
||||
{
|
||||
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() { }
|
||||
- `Register<T>(handler, priority)`:按优先级订阅
|
||||
- `RegisterWithContext<T>(...)`:拿到 `EventContext<T>`
|
||||
- `EventPropagation.All`:广播给全部监听器
|
||||
- `EventPropagation.UntilHandled`:直到上下文事件被标记为 handled
|
||||
- `EventPropagation.Highest`:只执行最高优先级层
|
||||
|
||||
public void DealDamage(Character attacker, Character target, int damage)
|
||||
{
|
||||
target.Health -= damage;
|
||||
## 局部事件对象
|
||||
|
||||
// 发送伤害事件
|
||||
this.SendEvent(new DamageDealtEvent
|
||||
{
|
||||
Attacker = attacker.Name,
|
||||
Target = target.Name,
|
||||
Damage = damage
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
如果事件只在一个对象或一个小模块内部流动,不必一定挂到 `EventBus`。当前仍可直接使用:
|
||||
|
||||
### Controller 中注册事件
|
||||
- `EasyEvent`
|
||||
- `Event<T>`
|
||||
- `Event<T1, T2>`
|
||||
- `OrEvent`
|
||||
- `EventListenerScope<TEvent>`
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Controller;
|
||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||
这类类型更适合局部组合和 UI/工具层内聚逻辑,不适合作为全局消息总线的替代品。
|
||||
|
||||
[ContextAware]
|
||||
public partial class GameController : IController
|
||||
{
|
||||
private IUnRegisterList _unregisterList = new UnRegisterList();
|
||||
## 与 Store / CQRS 的边界
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// 注册多个事件
|
||||
this.RegisterEvent<GameStartedEvent>(OnGameStarted)
|
||||
.AddToUnregisterList(_unregisterList);
|
||||
- 轻量运行时广播:`EventBus`
|
||||
- 聚合状态演进:`Store<TState>`,必要时用 `BridgeToEventBus(...)` 兼容旧事件消费者
|
||||
- 新业务请求模型:`GFramework.Cqrs`
|
||||
|
||||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied)
|
||||
.AddToUnregisterList(_unregisterList);
|
||||
|
||||
this.RegisterEvent<LevelCompletedEvent>(OnLevelCompleted)
|
||||
.AddToUnregisterList(_unregisterList);
|
||||
}
|
||||
|
||||
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;如果你只是广播
|
||||
“这件事发生了”,事件系统更直接。
|
||||
|
||||
@ -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) 栏目
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
|
||||
@ -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
|
||||
{
|
||||
LoggerFactoryProvider = new ConsoleLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Debug
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
protected override void Write(LogLevel level, string message, Exception? exception)
|
||||
如果你只是想减少噪音或临时打开 `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)
|
||||
{
|
||||
// 自定义日志输出逻辑
|
||||
var logMessage = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
|
||||
if (exception != null)
|
||||
logMessage += $"\n{exception}";
|
||||
|
||||
Console.WriteLine(logMessage);
|
||||
structured.Log(
|
||||
LogLevel.Info,
|
||||
"Player matched",
|
||||
("PlayerId", playerId),
|
||||
("RoomId", roomId));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ConsoleLogger
|
||||
## 当前仓库内置的常用实现
|
||||
|
||||
控制台日志记录器实现,支持彩色输出。
|
||||
- `ConsoleLoggerFactoryProvider`
|
||||
- `ConsoleLoggerFactory`
|
||||
- `CompositeLogger`
|
||||
- `LoggingConfigurationLoader`
|
||||
|
||||
**使用示例:**
|
||||
如果你需要文件输出、rolling file、async appender 或 JSON formatter,可以先用
|
||||
`LoggingConfigurationLoader` 读取 `LoggingConfiguration`,再把自定义 `ILoggerFactoryProvider` 挂到
|
||||
`ArchitectureConfiguration.LoggerProperties.LoggerFactoryProvider` 或 `LoggerFactoryResolver.Provider`。
|
||||
|
||||
```csharp
|
||||
// 创建控制台日志记录器
|
||||
var logger = new ConsoleLogger("MyLogger", LogLevel.Debug);
|
||||
## 什么时候该换 provider
|
||||
|
||||
// 记录不同级别的日志
|
||||
logger.Info("应用程序启动");
|
||||
logger.Debug("调试信息");
|
||||
logger.Warn("警告信息");
|
||||
logger.Error("错误信息");
|
||||
logger.Fatal("致命错误");
|
||||
```
|
||||
下面这些场景通常不该只靠改 `MinLevel`:
|
||||
|
||||
**输出格式:**
|
||||
- 需要文件输出、rolling file 或 async appender
|
||||
- 需要按 namespace / level 做过滤
|
||||
- 需要 JSON 格式日志
|
||||
- 需要组合多个 appender
|
||||
|
||||
```
|
||||
[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)
|
||||
{
|
||||
Console.WriteLine($"[{level}] {message}");
|
||||
if (exception != null)
|
||||
Console.WriteLine(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 日志级别说明
|
||||
|
||||
| 级别 | 说明 | 使用场景 |
|
||||
|-------------|----------|-------------------|
|
||||
| **Trace** | 最详细的跟踪信息 | 调试复杂的执行流程,记录函数调用等 |
|
||||
| **Debug** | 调试信息 | 开发阶段,记录变量值、流程分支等 |
|
||||
| **Info** | 一般信息 | 记录重要的业务流程和系统状态 |
|
||||
| **Warning** | 警告信息 | 可能的问题但不中断程序执行 |
|
||||
| **Error** | 错误信息 | 影响功能但不致命的问题 |
|
||||
| **Fatal** | 致命错误 | 导致程序无法继续运行的严重错误 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用合适的日志级别**:
|
||||
- 使用 `Info` 记录重要业务流程
|
||||
- 使用 `Debug` 记录调试信息
|
||||
- 使用 `Warning` 记录异常情况
|
||||
- 使用 `Error` 记录错误但不影响程序运行
|
||||
- 使用 `Fatal` 记录严重错误
|
||||
|
||||
2. **提供上下文信息**:
|
||||
```csharp
|
||||
logger.Info($"用户登录成功: UserId={userId}, UserName={userName}");
|
||||
```
|
||||
|
||||
3. **异常日志记录**:
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
// 业务逻辑
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error("数据库操作失败", ex);
|
||||
}
|
||||
```
|
||||
|
||||
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 组合。
|
||||
|
||||
@ -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);
|
||||
如果状态已经演化为下面这些形态,更适合用 `Store<TState>`:
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 自动计算百分比
|
||||
Action updatePercent = () =>
|
||||
{
|
||||
HealthPercent.Value = (float)Health.Value / MaxHealth.Value;
|
||||
};
|
||||
- 多个字段必须作为一个原子状态一起演进
|
||||
- 多个模块共享同一聚合状态
|
||||
- 需要 reducer / middleware / 历史回放
|
||||
- 需要从整棵状态树中复用局部选择逻辑
|
||||
|
||||
Health.Register(_ => updatePercent());
|
||||
MaxHealth.Register(_ => updatePercent());
|
||||
迁移时不必一次性抛弃旧绑定风格。当前已经提供:
|
||||
|
||||
updatePercent(); // 初始计算
|
||||
}
|
||||
}
|
||||
```
|
||||
- `store.Select(...)`
|
||||
- `store.ToBindableProperty(...)`
|
||||
|
||||
### 3. 属性验证
|
||||
|
||||
```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. 条件监听
|
||||
|
||||
```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 的读取方式。
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 代码解析
|
||||
|
||||
生成的代码包含以下关键部分:
|
||||
|
||||
1. **私有字段**:
|
||||
- `_context`:缓存的上下文实例
|
||||
- `_contextProvider`:静态上下文提供者(所有实例共享)
|
||||
|
||||
2. **Context 属性**:
|
||||
- `protected` 访问级别,子类可访问
|
||||
- 懒加载:首次访问时自动初始化
|
||||
- 使用 `GameContextProvider` 作为默认提供者
|
||||
|
||||
3. **配置方法**:
|
||||
- `SetContextProvider()`:设置自定义上下文提供者
|
||||
- `ResetContextProvider()`:重置为默认提供者
|
||||
|
||||
4. **显式接口实现**:
|
||||
- `IContextAware.SetContext()`:允许外部设置上下文
|
||||
- `IContextAware.GetContext()`:返回当前上下文
|
||||
|
||||
## 配置上下文提供者
|
||||
|
||||
### 测试场景
|
||||
|
||||
在单元测试中,通常需要使用自定义的上下文提供者:
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public async Task TestPlayerController()
|
||||
{
|
||||
// 创建测试架构
|
||||
var testArchitecture = new TestArchitecture();
|
||||
await testArchitecture.InitAsync();
|
||||
|
||||
// 配置自定义上下文提供者
|
||||
PlayerController.SetContextProvider(new TestContextProvider(testArchitecture));
|
||||
|
||||
try
|
||||
{
|
||||
// 测试代码
|
||||
var controller = new PlayerController();
|
||||
controller.Initialize();
|
||||
|
||||
// 验证...
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 清理:重置上下文提供者
|
||||
}
|
||||
finally
|
||||
{
|
||||
PlayerController.ResetContextProvider();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多架构场景
|
||||
需要注意两点:
|
||||
|
||||
在某些高级场景中,可能需要同时运行多个架构实例:
|
||||
- `ResetContextProvider()` 只会重置共享 provider,不会清除已创建实例上的 `_context`
|
||||
- 如果测试要复用同一实例并切换上下文,应该显式调用 `((IContextAware)instance).SetContext(...)`
|
||||
|
||||
```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);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
- 非 `class` 会触发 `GF_Rule_001`
|
||||
- 非 `partial` 不会生成实现,并会触发公共 partial 约束诊断
|
||||
- 嵌套、字段注入等其他错误通常由对应的 Context Get 生成器和其诊断补充报告
|
||||
|
||||
## 使用场景
|
||||
## 与旧写法的边界
|
||||
|
||||
### 何时使用 [ContextAware]
|
||||
下面这些旧说法已经不够准确:
|
||||
|
||||
推荐在以下场景使用 `[ContextAware]` 属性:
|
||||
- “`[ContextAware]` 只是帮你补一个简单的 `GetContext()`”
|
||||
- “切换 provider 后,已有实例会自动跟着切换”
|
||||
- “`[ContextAware]` 和 `ContextAwareBase` 的默认行为完全一致”
|
||||
|
||||
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>();
|
||||
- 生成实现带有实例缓存、类型级共享 provider 和同步锁
|
||||
- provider 切换只影响尚未缓存上下文的实例
|
||||
- `ContextAwareBase` 是更轻量的实例级缓存路径
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ✅ 正确:在初始化方法中访问
|
||||
public void Initialize()
|
||||
{
|
||||
var model = this.GetModel<SomeModel>(); // 安全
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 优先使用 Context 属性而非接口方法
|
||||
|
||||
```csharp
|
||||
[ContextAware]
|
||||
public partial class MyController
|
||||
{
|
||||
public void DoSomething()
|
||||
{
|
||||
// ✅ 推荐:使用扩展方法
|
||||
var model = this.GetModel<SomeModel>();
|
||||
|
||||
// ❌ 不推荐:显式调用接口方法
|
||||
var context = ((IContextAware)this).GetContext();
|
||||
var model2 = context.GetModel<SomeModel>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 诊断信息
|
||||
|
||||
生成器会在以下情况报告编译错误:
|
||||
|
||||
### GFSG001: 类必须是 partial
|
||||
|
||||
```csharp
|
||||
[ContextAware]
|
||||
public class MyController { } // 错误:缺少 partial 关键字
|
||||
```
|
||||
|
||||
**解决方案**:添加 `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`
|
||||
|
||||
@ -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)
|
||||
{
|
||||
// 最后备选
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与 PriorityUsageAnalyzer 集成
|
||||
|
||||
### GF_Priority_Usage_001 诊断
|
||||
|
||||
`PriorityUsageAnalyzer` 分析器会检测应该使用 `GetAllByPriority<T>()` 而非 `GetAll<T>()` 的场景:
|
||||
|
||||
**错误示例**:
|
||||
|
||||
```csharp
|
||||
// ❌ 不推荐:可能未按优先级排序
|
||||
var systems = context.GetAll<ISystem>();
|
||||
```
|
||||
|
||||
**正确示例**:
|
||||
|
||||
```csharp
|
||||
// ✅ 推荐:确保按优先级排序
|
||||
var systems = context.GetAllByPriority<ISystem>();
|
||||
```
|
||||
|
||||
### 分析器规则
|
||||
|
||||
当满足以下条件时,分析器会报告 `GF_Priority_Usage_001` 诊断:
|
||||
|
||||
1. 类型实现了 `IPrioritized` 接口
|
||||
2. 使用了 `GetAll<T>()` 方法
|
||||
3. 建议改用 `GetAllByPriority<T>()` 方法
|
||||
|
||||
## 诊断信息
|
||||
|
||||
### 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}");
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
system.Init();
|
||||
}
|
||||
public int Priority => _enabled ? PriorityGroup.High : PriorityGroup.Deferred;
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
## 当前诊断与约束
|
||||
|
||||
### Q: 优先级值可以是任意整数吗?
|
||||
`[Priority]` 当前有几条直接约束:
|
||||
|
||||
**A**: 是的,优先级值可以是任何 `int` 类型的值。推荐使用 `PriorityGroup` 预定义常量或在项目中定义自己的优先级常量。
|
||||
- `GF_Priority_001`
|
||||
- 只能标在 `class`
|
||||
- `GF_Priority_002`
|
||||
- 目标类型已经手写实现 `IPrioritized`
|
||||
- `GF_Priority_003`
|
||||
- 类型必须是 `partial`
|
||||
- `GF_Priority_004`
|
||||
- 特性值缺失或无效
|
||||
- `GF_Priority_005`
|
||||
- 不支持嵌套类
|
||||
|
||||
### Q: 多个类有相同优先级会怎样?
|
||||
对文档而言,最关键的结论是:
|
||||
|
||||
**A**: 当多个类有相同的优先级值时,它们的相对顺序是不确定的。建议为每个类设置不同的优先级值,或在文档中明确说明相同优先级类的执行顺序不保证。
|
||||
- `partial` 是强约束
|
||||
- 顶层类是强约束
|
||||
- 手写实现与生成实现只能二选一
|
||||
|
||||
### Q: 可以在运行时改变优先级吗?
|
||||
## 与旧写法的边界
|
||||
|
||||
**A**: 不可以。`Priority` 属性是只读的,值在编译时确定。如果需要运行时改变优先级,应手动实现 `IPrioritized` 接口。
|
||||
下面这些旧写法或旧表述已经不再适合作为默认指导:
|
||||
|
||||
```csharp
|
||||
public class DynamicPrioritySystem : IPrioritized
|
||||
{
|
||||
private int _priority;
|
||||
- 在 `IContextAware` 类型里统一写 `this.GetAllByPriority<T>()`
|
||||
- 继续用 `system.Init()` 作为系统初始化示例
|
||||
- 把 `[Priority]` 写成“标了就会自动改变执行顺序”
|
||||
|
||||
public int Priority => _priority;
|
||||
当前更准确的理解是:
|
||||
|
||||
public void SetPriority(int value)
|
||||
{
|
||||
_priority = value;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `[Priority]` 只生成 `Priority`
|
||||
- 排序效果依赖容器、上下文或扩展方法是否走了 priority-aware API
|
||||
- `IContextAware` 路径更推荐按组件类别使用 `GetSystemsByPriority` / `GetServicesByPriority` 等入口
|
||||
|
||||
### 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`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user