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
|
- 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
|
- 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`
|
- 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.
|
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.
|
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`.
|
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:
|
- Default:
|
||||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py`
|
- `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:
|
- Force a PR number:
|
||||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253`
|
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --pr 253`
|
||||||
- Machine-readable output:
|
- Machine-readable output:
|
||||||
- `python3 .codex/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json`
|
- `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
|
## Output Expectations
|
||||||
|
|
||||||
@ -47,6 +57,7 @@ The script should produce:
|
|||||||
- Pre-merge failed checks, if present
|
- Pre-merge failed checks, if present
|
||||||
- Latest MegaLinter status and any detailed issues posted by `github-actions[bot]`
|
- Latest MegaLinter status and any detailed issues posted by `github-actions[bot]`
|
||||||
- Test summary, including failed-test signals when present
|
- 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
|
- Parse warnings only when both the primary API source and the intended fallback signal are unavailable
|
||||||
|
|
||||||
## Recovery Rules
|
## 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.
|
- 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.
|
- 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.
|
- 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
|
## Example Triggers
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import argparse
|
|||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
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)
|
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
|
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
|
||||||
REQUEST_TIMEOUT_ENVIRONMENT_KEY = "GFRAMEWORK_PR_REVIEW_TIMEOUT_SECONDS"
|
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:
|
def resolve_git_command() -> str:
|
||||||
@ -153,6 +165,14 @@ def collapse_whitespace(text: str) -> str:
|
|||||||
return re.sub(r"\s+", " ", text).strip()
|
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:
|
def strip_tags(text: str) -> str:
|
||||||
return collapse_whitespace(re.sub(r"<[^>]+>", " ", text))
|
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"]))
|
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]:
|
def fetch_latest_commit_review(pr_number: int) -> dict[str, Any]:
|
||||||
api_base = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}"
|
api_base = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}"
|
||||||
commits = fetch_paged_json(f"{api_base}/commits?per_page=100")
|
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")
|
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")]
|
candidate_reviews = latest_commit_reviews or [review for review in reviews if review.get("submitted_at")]
|
||||||
latest_review = (
|
latest_review = select_latest_submitted_review(candidate_reviews)
|
||||||
max(candidate_reviews, key=lambda review: review.get("submitted_at", ""))
|
latest_coderabbit_review_with_body = select_latest_submitted_review(
|
||||||
if candidate_reviews
|
candidate_reviews,
|
||||||
else None
|
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]
|
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 "",
|
"user": latest_review.get("user", {}).get("login") if latest_review else "",
|
||||||
"body": latest_review.get("body") 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,
|
"threads": threads,
|
||||||
"open_threads": open_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] = {}
|
coderabbit_review: dict[str, Any] = {}
|
||||||
try:
|
try:
|
||||||
latest_commit_review = fetch_latest_commit_review(pr_number)
|
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 "")
|
latest_review_body = str(latest_review.get("body") or "")
|
||||||
if latest_review.get("user") == CODERABBIT_LOGIN and latest_review_body:
|
if latest_review.get("user") == CODERABBIT_LOGIN and latest_review_body:
|
||||||
coderabbit_review = parse_latest_review_body(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] = []
|
lines: list[str] = []
|
||||||
|
selected_sections = set(sections or DISPLAY_SECTION_CHOICES)
|
||||||
|
normalized_path_filters = normalize_path_filters(path_filters)
|
||||||
pr = result["pull_request"]
|
pr = result["pull_request"]
|
||||||
lines.append(f"PR #{pr['number']}: {pr['title']}")
|
if "pr" in selected_sections:
|
||||||
lines.append(f"State: {pr['state']}")
|
lines.append(f"PR #{pr['number']}: {pr['title']}")
|
||||||
lines.append(f"Branch: {pr['head_branch']} -> {pr['base_branch']}")
|
lines.append(f"State: {pr['state']}")
|
||||||
lines.append(f"URL: {pr['url']}")
|
lines.append(f"Branch: {pr['head_branch']} -> {pr['base_branch']}")
|
||||||
|
lines.append(f"URL: {pr['url']}")
|
||||||
|
|
||||||
failed_checks = result["coderabbit_summary"].get("failed_checks", [])
|
failed_checks = result["coderabbit_summary"].get("failed_checks", [])
|
||||||
lines.append("")
|
if "failed-checks" in selected_sections:
|
||||||
lines.append(f"Failed checks: {len(failed_checks)}")
|
lines.append("")
|
||||||
for check in failed_checks:
|
lines.append(f"Failed checks: {len(failed_checks)}")
|
||||||
lines.append(f"- {check['name']}: {check['status']}")
|
for check in failed_checks:
|
||||||
lines.append(f" Explanation: {check['explanation']}")
|
lines.append(f"- {check['name']}: {check['status']}")
|
||||||
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", {})
|
coderabbit_comments = result.get("coderabbit_comments", {})
|
||||||
review_feedback = result.get("coderabbit_review", {})
|
review_feedback = result.get("coderabbit_review", {})
|
||||||
comments = coderabbit_comments.get("comments", [])
|
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)
|
actionable_count = review_feedback.get("actionable_count") or coderabbit_comments.get("count") or len(comments)
|
||||||
lines.append("")
|
if "actionable" in selected_sections:
|
||||||
lines.append(f"CodeRabbit actionable comments: {actionable_count}")
|
lines.append("")
|
||||||
for comment in comments:
|
lines.append(
|
||||||
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
|
f"CodeRabbit actionable comments: {actionable_count} total"
|
||||||
if comment["title"]:
|
+ (
|
||||||
lines.append(f" Title: {comment['title']}")
|
f", {len(visible_comments)} shown after path filter"
|
||||||
if comment["description"]:
|
if normalized_path_filters
|
||||||
lines.append(f" Description: {comment['description']}")
|
else ""
|
||||||
if actionable_count and not comments:
|
)
|
||||||
lines.append(" Details: see latest-commit review threads below.")
|
)
|
||||||
|
for comment in visible_comments:
|
||||||
|
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
|
||||||
|
if comment["title"]:
|
||||||
|
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
|
||||||
|
if comment["description"]:
|
||||||
|
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
|
||||||
|
if actionable_count and not visible_comments:
|
||||||
|
lines.append(" Details: no actionable comments matched the current path filter.")
|
||||||
|
elif actionable_count and not comments:
|
||||||
|
lines.append(" Details: see latest-commit review threads below.")
|
||||||
|
|
||||||
outside_diff_comments = review_feedback.get("outside_diff_comments", [])
|
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)
|
outside_diff_count = review_feedback.get("outside_diff_count") or len(outside_diff_comments)
|
||||||
lines.append("")
|
if "outside-diff" in selected_sections:
|
||||||
lines.append(f"CodeRabbit outside-diff comments: {outside_diff_count} declared, {len(outside_diff_comments)} parsed")
|
lines.append("")
|
||||||
for comment in outside_diff_comments:
|
lines.append(
|
||||||
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
|
f"CodeRabbit outside-diff comments: {outside_diff_count} declared, {len(outside_diff_comments)} parsed"
|
||||||
if comment["title"]:
|
+ (
|
||||||
lines.append(f" Title: {comment['title']}")
|
f", {len(visible_outside_diff_comments)} shown after path filter"
|
||||||
if comment["description"]:
|
if normalized_path_filters
|
||||||
lines.append(f" Description: {comment['description']}")
|
else ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for comment in visible_outside_diff_comments:
|
||||||
|
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
|
||||||
|
if comment["title"]:
|
||||||
|
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
|
||||||
|
if comment["description"]:
|
||||||
|
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
|
||||||
|
if outside_diff_comments and not visible_outside_diff_comments:
|
||||||
|
lines.append(" Details: no outside-diff comments matched the current path filter.")
|
||||||
|
|
||||||
nitpick_comments = review_feedback.get("nitpick_comments", [])
|
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)
|
nitpick_count = review_feedback.get("nitpick_count") or len(nitpick_comments)
|
||||||
lines.append("")
|
if "nitpick" in selected_sections:
|
||||||
lines.append(f"CodeRabbit nitpick comments: {nitpick_count} declared, {len(nitpick_comments)} parsed")
|
lines.append("")
|
||||||
for comment in nitpick_comments:
|
lines.append(
|
||||||
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
|
f"CodeRabbit nitpick comments: {nitpick_count} declared, {len(nitpick_comments)} parsed"
|
||||||
if comment["title"]:
|
+ (
|
||||||
lines.append(f" Title: {comment['title']}")
|
f", {len(visible_nitpick_comments)} shown after path filter"
|
||||||
if comment["description"]:
|
if normalized_path_filters
|
||||||
lines.append(f" Description: {comment['description']}")
|
else ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for comment in visible_nitpick_comments:
|
||||||
|
lines.append(f"- {comment['path']} {comment['range']}".rstrip())
|
||||||
|
if comment["title"]:
|
||||||
|
lines.append(f" Title: {truncate_text(comment['title'], max_description_length)}")
|
||||||
|
if comment["description"]:
|
||||||
|
lines.append(f" Description: {truncate_text(comment['description'], max_description_length)}")
|
||||||
|
if nitpick_comments and not visible_nitpick_comments:
|
||||||
|
lines.append(" Details: no nitpick comments matched the current path filter.")
|
||||||
|
|
||||||
latest_commit_review = result.get("latest_commit_review", {})
|
latest_commit_review = result.get("latest_commit_review", {})
|
||||||
latest_commit = latest_commit_review.get("latest_commit", {})
|
latest_commit = latest_commit_review.get("latest_commit", {})
|
||||||
latest_review = latest_commit_review.get("latest_review", {})
|
latest_review = latest_commit_review.get("latest_review", {})
|
||||||
open_threads = latest_commit_review.get("open_threads", [])
|
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("")
|
||||||
lines.append(f"Latest reviewed commit: {latest_commit.get('sha', '')}")
|
lines.append(f"Latest reviewed commit: {latest_commit.get('sha', '')}")
|
||||||
if latest_review:
|
if latest_review:
|
||||||
@ -746,23 +878,32 @@ def format_text(result: dict[str, Any]) -> str:
|
|||||||
lines.append(
|
lines.append(
|
||||||
"Latest commit review threads: "
|
"Latest commit review threads: "
|
||||||
f"{len(latest_commit_review.get('threads', []))} total, {len(open_threads)} open"
|
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"]
|
root_comment = thread["root_comment"]
|
||||||
latest_comment = thread["latest_comment"]
|
latest_comment = thread["latest_comment"]
|
||||||
lines.append(f"- {thread['path']}:{thread['line']}")
|
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"]:
|
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(
|
if contains_visible_addressed_commit_text(root_comment["body"]) or contains_visible_addressed_commit_text(
|
||||||
latest_comment["body"]
|
latest_comment["body"]
|
||||||
):
|
):
|
||||||
lines.append(
|
lines.append(
|
||||||
" Note: thread is still open; treat the visible 'Addressed in commit ...' text as unverified until local code matches."
|
" 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", {})
|
megalinter_report = result.get("megalinter_report", {})
|
||||||
if megalinter_report:
|
if megalinter_report and "megalinter" in selected_sections:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(
|
lines.append(
|
||||||
"MegaLinter: "
|
"MegaLinter: "
|
||||||
@ -784,32 +925,37 @@ def format_text(result: dict[str, Any]) -> str:
|
|||||||
|
|
||||||
for issue in megalinter_report.get("detailed_issues", []):
|
for issue in megalinter_report.get("detailed_issues", []):
|
||||||
lines.append(f"- Detailed issue: {issue['summary']}")
|
lines.append(f"- Detailed issue: {issue['summary']}")
|
||||||
lines.append(f" {collapse_whitespace(issue['details'])}")
|
lines.append(f" {truncate_text(issue['details'], max_description_length)}")
|
||||||
|
|
||||||
lines.append("")
|
if "tests" in selected_sections:
|
||||||
lines.append(f"Test reports: {len(result['test_reports'])}")
|
lines.append("")
|
||||||
for index, report in enumerate(result["test_reports"], start=1):
|
lines.append(f"Test reports: {len(result['test_reports'])}")
|
||||||
stats = report.get("stats", {})
|
for index, report in enumerate(result["test_reports"], start=1):
|
||||||
if stats:
|
stats = report.get("stats", {})
|
||||||
lines.append(
|
if stats:
|
||||||
f"- Report {index}: tests={stats.get('tests')} passed={stats.get('passed')} "
|
lines.append(
|
||||||
f"failed={stats.get('failed')} skipped={stats.get('skipped')} flaky={stats.get('flaky')} "
|
f"- Report {index}: tests={stats.get('tests')} passed={stats.get('passed')} "
|
||||||
f"duration={stats.get('duration')}"
|
f"failed={stats.get('failed')} skipped={stats.get('skipped')} flaky={stats.get('flaky')} "
|
||||||
)
|
f"duration={stats.get('duration')}"
|
||||||
else:
|
)
|
||||||
lines.append(f"- Report {index}: no structured test stats parsed")
|
else:
|
||||||
|
lines.append(f"- Report {index}: no structured test stats parsed")
|
||||||
|
|
||||||
if report["has_failed_tests"]:
|
if report["has_failed_tests"]:
|
||||||
for failed_test in report["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:
|
else:
|
||||||
lines.append(" Failed tests: none reported")
|
lines.append(" Failed tests: none reported")
|
||||||
|
|
||||||
if result["parse_warnings"]:
|
if result["parse_warnings"] and "warnings" in selected_sections:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Warnings:")
|
lines.append("Warnings:")
|
||||||
for warning in result["parse_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)
|
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("--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("--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("--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()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@ -832,12 +999,27 @@ def main() -> None:
|
|||||||
pr_number = resolve_pr_number(branch)
|
pr_number = resolve_pr_number(branch)
|
||||||
|
|
||||||
result = build_result(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 args.format == "json":
|
||||||
|
if json_output_path:
|
||||||
|
print(json_output_path)
|
||||||
|
return
|
||||||
|
|
||||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
return
|
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__":
|
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.
|
- 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`.
|
- 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
|
- 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.
|
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
|
- 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));
|
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>
|
||||||
/// 验证协程调度器应该使用提供的时间源
|
/// 验证协程调度器应该使用提供的时间源
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -563,4 +577,4 @@ public class TestTimeSource : ITimeSource
|
|||||||
DeltaTime = 0.1;
|
DeltaTime = 0.1;
|
||||||
CurrentTime += DeltaTime;
|
CurrentTime += DeltaTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,6 +124,24 @@ public class EventTests
|
|||||||
Assert.That(values, Does.Contain(10));
|
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>
|
||||||
/// 测试带两个泛型参数的事件注册功能是否正确添加处理器
|
/// 测试带两个泛型参数的事件注册功能是否正确添加处理器
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -161,4 +179,22 @@ public class EventTests
|
|||||||
_eventIntString.Trigger(2, "b");
|
_eventIntString.Trigger(2, "b");
|
||||||
Assert.That(count, Is.EqualTo(1));
|
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);
|
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>
|
/// <summary>
|
||||||
/// 验证并发Push是线程安全的
|
/// 验证并发Push是线程安全的
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -469,4 +498,4 @@ public class PauseStackManagerTests
|
|||||||
LastIsPaused = null;
|
LastIsPaused = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.StateManagement;
|
namespace GFramework.Core.Tests.StateManagement;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -384,6 +387,32 @@ public class StoreTests
|
|||||||
Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" }));
|
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>
|
/// <summary>
|
||||||
/// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。
|
/// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -681,6 +710,32 @@ public class StoreTests
|
|||||||
return store;
|
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>
|
/// <summary>
|
||||||
/// 用于测试的计数器状态。
|
/// 用于测试的计数器状态。
|
||||||
/// 使用 record 保持逻辑不可变语义,便于 Store 基于状态快照进行比较和断言。
|
/// 使用 record 保持逻辑不可变语义,便于 Store 基于状态快照进行比较和断言。
|
||||||
@ -876,4 +931,20 @@ public class StoreTests
|
|||||||
next();
|
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");
|
logger.Info($"Initializing {_pendingInitializableList.Count} components");
|
||||||
|
|
||||||
// 按类型分组初始化(保持原有的阶段划分)
|
var initializationPlan = CreateInitializationPlan();
|
||||||
var utilities = _pendingInitializableList.OfType<IContextUtility>().ToList();
|
|
||||||
var models = _pendingInitializableList.OfType<IModel>().ToList();
|
|
||||||
var systems = _pendingInitializableList.OfType<ISystem>().ToList();
|
|
||||||
|
|
||||||
// 1. 工具初始化阶段
|
await InitializePhaseComponentsAsync(
|
||||||
EnterPhase(ArchitecturePhase.BeforeUtilityInit);
|
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)
|
MarkInitializationCompleted();
|
||||||
{
|
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -223,6 +188,67 @@ internal sealed class ArchitectureLifecycle(
|
|||||||
component.Initialize();
|
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>
|
/// <summary>
|
||||||
/// 立即初始化在常规初始化批次完成后新增的组件。
|
/// 立即初始化在常规初始化批次完成后新增的组件。
|
||||||
/// 当启用 <c>AllowLateRegistration</c> 时,生命周期层需要和注册层保持一致,
|
/// 当启用 <c>AllowLateRegistration</c> 时,生命周期层需要和注册层保持一致,
|
||||||
@ -258,6 +284,17 @@ internal sealed class ArchitectureLifecycle(
|
|||||||
|
|
||||||
#endregion
|
#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
|
#region Ready State
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using GFramework.Core.Abstractions.Command;
|
using GFramework.Core.Abstractions.Command;
|
||||||
using GFramework.Core.Rule;
|
using GFramework.Core.Rule;
|
||||||
|
using GFramework.Cqrs.Abstractions.Cqrs.Command;
|
||||||
|
|
||||||
namespace GFramework.Core.Command;
|
namespace GFramework.Core.Command;
|
||||||
|
|
||||||
@ -26,3 +27,54 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand
|
|||||||
/// <returns>表示异步操作的任务</returns>
|
/// <returns>表示异步操作的任务</returns>
|
||||||
protected abstract Task OnExecuteAsync();
|
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.Abstractions.Command;
|
||||||
using GFramework.Core.Rule;
|
using GFramework.Core.Rule;
|
||||||
|
using GFramework.Cqrs.Abstractions.Cqrs.Command;
|
||||||
|
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
|
||||||
|
|
||||||
namespace GFramework.Core.Command;
|
namespace GFramework.Core.Command;
|
||||||
|
|
||||||
@ -20,4 +22,55 @@ public abstract class AbstractCommand : ContextAwareBase, ICommand
|
|||||||
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
|
/// 命令执行的抽象方法,由派生类实现具体的命令逻辑
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected abstract void OnExecute();
|
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>
|
/// </remarks>
|
||||||
/// <param name="timeSource">缩放时间源,提供调度器默认推进所使用的时间数据。</param>
|
/// <param name="timeSource">缩放时间源,提供调度器默认推进所使用的时间数据。</param>
|
||||||
/// <param name="instanceId">协程实例编号,用于生成带宿主前缀的句柄。</param>
|
/// <param name="instanceId">协程实例编号,用于生成带宿主前缀的句柄。</param>
|
||||||
/// <param name="initialCapacity">调度器初始槽位容量。</param>
|
/// <param name="initialCapacity">调度器初始槽位容量;允许为 0,此时首次启动协程会按需自动扩容。</param>
|
||||||
/// <param name="enableStatistics">是否启用协程统计功能。</param>
|
/// <param name="enableStatistics">是否启用协程统计功能。</param>
|
||||||
/// <param name="realtimeTimeSource">
|
/// <param name="realtimeTimeSource">
|
||||||
/// 非缩放时间源。
|
/// 非缩放时间源。
|
||||||
@ -211,58 +211,10 @@ public sealed class CoroutineScheduler(
|
|||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_nextSlot >= _slots.Length)
|
|
||||||
{
|
|
||||||
Expand();
|
|
||||||
}
|
|
||||||
|
|
||||||
var handle = new CoroutineHandle(instanceId);
|
var handle = new CoroutineHandle(instanceId);
|
||||||
var slotIndex = _nextSlot++;
|
var slotIndex = AllocateSlotIndex();
|
||||||
|
var slot = CreateRunningSlot(handle, coroutine, priority, cancellationToken);
|
||||||
var slot = new CoroutineSlot
|
RegisterStartedCoroutine(handle, slotIndex, slot, priority, tag, group);
|
||||||
{
|
|
||||||
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++;
|
|
||||||
|
|
||||||
Prewarm(slotIndex);
|
Prewarm(slotIndex);
|
||||||
UpdateStatisticsSnapshot();
|
UpdateStatisticsSnapshot();
|
||||||
|
|
||||||
@ -662,70 +614,14 @@ public sealed class CoroutineScheduler(
|
|||||||
CoroutineCompletionStatus completionStatus,
|
CoroutineCompletionStatus completionStatus,
|
||||||
Exception? exception = null)
|
Exception? exception = null)
|
||||||
{
|
{
|
||||||
var slot = _slots[slotIndex];
|
if (!TryGetFinalizableCoroutine(slotIndex, out var slot, out var handle))
|
||||||
if (slot == null)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = slot.Handle;
|
UpdateCompletionMetadata(handle, completionStatus);
|
||||||
if (!handle.IsValid)
|
ReleaseCompletedCoroutine(slotIndex, slot, handle);
|
||||||
{
|
CompleteCoroutineLifecycle(handle, completionStatus);
|
||||||
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);
|
|
||||||
OnCoroutineFinished?.Invoke(handle, completionStatus, exception);
|
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>
|
||||||
/// 释放单个槽位持有的资源。
|
/// 释放单个槽位持有的资源。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -824,6 +853,125 @@ public sealed class CoroutineScheduler(
|
|||||||
slot.Waiting = null;
|
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>
|
||||||
/// 唤醒所有等待目标协程完成的协程。
|
/// 唤醒所有等待目标协程完成的协程。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -888,7 +1036,9 @@ public sealed class CoroutineScheduler(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void Expand()
|
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>
|
/// <summary>
|
||||||
|
|||||||
@ -3,24 +3,24 @@
|
|||||||
namespace GFramework.Core.Events;
|
namespace GFramework.Core.Events;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 泛型事件类,支持一个泛型参数 T 的事件注册、注销与触发。
|
/// 泛型事件类,支持一个泛型参数 <typeparamref name="T" /> 的事件注册、注销与触发。
|
||||||
/// 实现了 IEvent 接口以提供统一的事件操作接口。
|
/// 实现了 <see cref="IEvent" /> 接口以提供统一的事件操作接口。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">事件回调函数的第一个参数类型。</typeparam>
|
/// <typeparam name="T">事件回调函数的第一个参数类型。</typeparam>
|
||||||
public class Event<T> : IEvent
|
public class Event<T> : IEvent
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 存储已注册的事件处理委托。
|
/// 存储已注册的事件处理委托。
|
||||||
/// 默认为空操作(no-op)委托,避免 null 检查。
|
/// 未注册监听器时保持 <see langword="null" />,从而让监听器计数与真实订阅数量保持一致。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Action<T>? _mOnEvent = _ => { };
|
private Action<T>? _mOnEvent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 显式实现 IEvent 接口中的 Register 方法。
|
/// 显式实现 <see cref="IEvent" /> 接口中的 <c>Register</c> 方法。
|
||||||
/// 允许使用无参 Action 来订阅当前带参事件。
|
/// 允许使用无参 <see cref="Action" /> 来订阅当前带参事件。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="onEvent">无参事件处理方法。</param>
|
/// <param name="onEvent">无参事件处理方法。</param>
|
||||||
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
|
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
|
||||||
IUnRegister IEvent.Register(Action onEvent)
|
IUnRegister IEvent.Register(Action onEvent)
|
||||||
{
|
{
|
||||||
return Register(Action);
|
return Register(Action);
|
||||||
@ -35,7 +35,7 @@ public class Event<T> : IEvent
|
|||||||
/// 注册一个事件监听器,并返回可用于取消注册的对象。
|
/// 注册一个事件监听器,并返回可用于取消注册的对象。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="onEvent">要注册的事件处理方法。</param>
|
/// <param name="onEvent">要注册的事件处理方法。</param>
|
||||||
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
|
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
|
||||||
public IUnRegister Register(Action<T> onEvent)
|
public IUnRegister Register(Action<T> onEvent)
|
||||||
{
|
{
|
||||||
_mOnEvent += onEvent;
|
_mOnEvent += onEvent;
|
||||||
@ -52,7 +52,7 @@ public class Event<T> : IEvent
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 触发所有已注册的事件处理程序,并传递参数 t。
|
/// 触发所有已注册的事件处理程序,并传递参数 <paramref name="t" />。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="t">传递给事件处理程序的参数。</param>
|
/// <param name="t">传递给事件处理程序的参数。</param>
|
||||||
public void Trigger(T t)
|
public void Trigger(T t)
|
||||||
@ -61,9 +61,9 @@ public class Event<T> : IEvent
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前已注册的监听器数量
|
/// 获取当前已注册的监听器数量。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>监听器数量</returns>
|
/// <returns>监听器数量。</returns>
|
||||||
public int GetListenerCount()
|
public int GetListenerCount()
|
||||||
{
|
{
|
||||||
return _mOnEvent?.GetInvocationList().Length ?? 0;
|
return _mOnEvent?.GetInvocationList().Length ?? 0;
|
||||||
@ -71,30 +71,30 @@ public class Event<T> : IEvent
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 支持两个泛型参数 T 和 TK 的事件类。
|
/// 支持两个泛型参数 <typeparamref name="T" /> 和 <typeparamref name="TK" /> 的事件类。
|
||||||
/// 提供事件注册、注销和触发功能。
|
/// 提供事件注册、注销和触发功能。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">第一个参数类型。</typeparam>
|
/// <typeparam name="T">第一个参数类型。</typeparam>
|
||||||
/// <typeparam name="Tk">第二个参数类型。</typeparam>
|
/// <typeparam name="TK">第二个参数类型。</typeparam>
|
||||||
public class Event<T, Tk> : IEvent
|
public class Event<T, TK> : IEvent
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 存储已注册的双参数事件处理委托。
|
/// 存储已注册的双参数事件处理委托。
|
||||||
/// 默认为空操作(no-op)委托。
|
/// 未注册监听器时保持 <see langword="null" />,从而让监听器计数与真实订阅数量保持一致。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Action<T, Tk>? _mOnEvent = (_, _) => { };
|
private Action<T, TK>? _mOnEvent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 显式实现 IEvent 接口中的 Register 方法。
|
/// 显式实现 <see cref="IEvent" /> 接口中的 <c>Register</c> 方法。
|
||||||
/// 允许使用无参 Action 来订阅当前带参事件。
|
/// 允许使用无参 <see cref="Action" /> 来订阅当前带参事件。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="onEvent">无参事件处理方法。</param>
|
/// <param name="onEvent">无参事件处理方法。</param>
|
||||||
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
|
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
|
||||||
IUnRegister IEvent.Register(Action onEvent)
|
IUnRegister IEvent.Register(Action onEvent)
|
||||||
{
|
{
|
||||||
return Register(Action);
|
return Register(Action);
|
||||||
|
|
||||||
void Action(T _, Tk __)
|
void Action(T _, TK __)
|
||||||
{
|
{
|
||||||
onEvent();
|
onEvent();
|
||||||
}
|
}
|
||||||
@ -104,8 +104,8 @@ public class Event<T, Tk> : IEvent
|
|||||||
/// 注册一个接受两个参数的事件监听器,并返回可用于取消注册的对象。
|
/// 注册一个接受两个参数的事件监听器,并返回可用于取消注册的对象。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="onEvent">要注册的事件处理方法。</param>
|
/// <param name="onEvent">要注册的事件处理方法。</param>
|
||||||
/// <returns>IUnRegister 对象,用于稍后注销该事件监听器。</returns>
|
/// <returns><see cref="IUnRegister" /> 对象,用于稍后注销该事件监听器。</returns>
|
||||||
public IUnRegister Register(Action<T, Tk> onEvent)
|
public IUnRegister Register(Action<T, TK> onEvent)
|
||||||
{
|
{
|
||||||
_mOnEvent += onEvent;
|
_mOnEvent += onEvent;
|
||||||
return new DefaultUnRegister(() => UnRegister(onEvent));
|
return new DefaultUnRegister(() => UnRegister(onEvent));
|
||||||
@ -115,27 +115,27 @@ public class Event<T, Tk> : IEvent
|
|||||||
/// 取消指定的双参数事件监听器。
|
/// 取消指定的双参数事件监听器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="onEvent">需要被注销的事件处理方法。</param>
|
/// <param name="onEvent">需要被注销的事件处理方法。</param>
|
||||||
public void UnRegister(Action<T, Tk> onEvent)
|
public void UnRegister(Action<T, TK> onEvent)
|
||||||
{
|
{
|
||||||
_mOnEvent -= onEvent;
|
_mOnEvent -= onEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 触发所有已注册的事件处理程序,并传递参数 t 和 k。
|
/// 触发所有已注册的事件处理程序,并传递参数 <paramref name="t" /> 和 <paramref name="k" />。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="t">第一个参数。</param>
|
/// <param name="t">第一个参数。</param>
|
||||||
/// <param name="k">第二个参数。</param>
|
/// <param name="k">第二个参数。</param>
|
||||||
public void Trigger(T t, Tk k)
|
public void Trigger(T t, TK k)
|
||||||
{
|
{
|
||||||
_mOnEvent?.Invoke(t, k);
|
_mOnEvent?.Invoke(t, k);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前已注册的监听器数量
|
/// 获取当前已注册的监听器数量。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>监听器数量</returns>
|
/// <returns>监听器数量。</returns>
|
||||||
public int GetListenerCount()
|
public int GetListenerCount()
|
||||||
{
|
{
|
||||||
return _mOnEvent?.GetInvocationList().Length ?? 0;
|
return _mOnEvent?.GetInvocationList().Length ?? 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -26,69 +26,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
|
|||||||
public ValueTask DestroyAsync()
|
public ValueTask DestroyAsync()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
{
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
|
|
||||||
List<PauseGroup> pausedGroups;
|
|
||||||
IPauseHandler[] handlersSnapshot;
|
|
||||||
|
|
||||||
_lock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
return ValueTask.CompletedTask;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
|
|
||||||
// 收集所有当前暂停的组
|
|
||||||
pausedGroups = _pauseStacks
|
|
||||||
.Where(kvp => kvp.Value.Count > 0)
|
|
||||||
.Select(kvp => kvp.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// 获取处理器快照
|
|
||||||
handlersSnapshot = _handlers.ToArray();
|
|
||||||
|
|
||||||
// 清理所有数据结构
|
|
||||||
_pauseStacks.Clear();
|
|
||||||
_tokenMap.Clear();
|
|
||||||
_handlers.Clear();
|
|
||||||
|
|
||||||
_logger.Debug("PauseStackManager destroyed");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.ExitWriteLock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在锁外通知所有之前暂停的组恢复,保持生命周期信号一致
|
var destroySnapshot = TryBeginDestroy();
|
||||||
foreach (var group in pausedGroups)
|
if (destroySnapshot == null)
|
||||||
{
|
{
|
||||||
_logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false");
|
return ValueTask.CompletedTask;
|
||||||
|
|
||||||
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();
|
_lock.Dispose();
|
||||||
|
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
@ -163,74 +111,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
|
|||||||
public bool Pop(PauseToken token)
|
public bool Pop(PauseToken token)
|
||||||
{
|
{
|
||||||
if (!token.IsValid)
|
if (!token.IsValid)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
bool found;
|
|
||||||
bool shouldNotify = false;
|
|
||||||
PauseGroup notifyGroup = PauseGroup.Global;
|
|
||||||
|
|
||||||
_lock.EnterWriteLock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
if (!_tokenMap.TryGetValue(token.Id, out var entry))
|
|
||||||
{
|
|
||||||
_logger.Warn($"Attempted to pop invalid/expired token: {token.Id}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var group = entry.Group;
|
|
||||||
var stack = _pauseStacks[group];
|
|
||||||
var wasPaused = stack.Count > 0;
|
|
||||||
|
|
||||||
// 从栈中移除
|
|
||||||
var tempStack = new Stack<PauseEntry>();
|
|
||||||
found = false;
|
|
||||||
|
|
||||||
while (stack.Count > 0)
|
|
||||||
{
|
|
||||||
var current = stack.Pop();
|
|
||||||
if (current.TokenId == token.Id)
|
|
||||||
{
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
tempStack.Push(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 恢复栈结构
|
|
||||||
while (tempStack.Count > 0)
|
|
||||||
{
|
|
||||||
stack.Push(tempStack.Pop());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found)
|
|
||||||
{
|
|
||||||
_tokenMap.Remove(token.Id);
|
|
||||||
_logger.Debug($"Pause popped: {entry.Reason} (Group: {group}, Remaining: {stack.Count})");
|
|
||||||
|
|
||||||
// 状态变化检测:从暂停 → 未暂停
|
|
||||||
if (wasPaused && stack.Count == 0)
|
|
||||||
{
|
|
||||||
shouldNotify = true;
|
|
||||||
notifyGroup = group;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_lock.ExitWriteLock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在锁外通知处理器,避免死锁
|
var result = TryPopEntry(token);
|
||||||
if (shouldNotify)
|
if (result.ShouldNotify)
|
||||||
{
|
{
|
||||||
NotifyHandlers(notifyGroup, false);
|
NotifyHandlers(result.NotifyGroup, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return found;
|
return result.Found;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
||||||
/// 内部查询暂停状态的方法,不加锁。
|
/// 内部查询暂停状态的方法,不加锁。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -467,7 +552,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
|
|||||||
_lock.EnterReadLock();
|
_lock.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
handlersSnapshot = _handlers.OrderBy(h => h.Priority).ToArray();
|
handlersSnapshot = CreateHandlerSnapshot();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -475,17 +560,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 在锁外遍历快照并通知处理器
|
// 在锁外遍历快照并通知处理器
|
||||||
foreach (var handler in handlersSnapshot)
|
NotifyHandlersSnapshot(group, isPaused, handlersSnapshot, isDestroying: false);
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
handler.OnPauseStateChanged(group, isPaused);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Error($"Handler {handler.GetType().Name} failed", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发事件
|
// 触发事件
|
||||||
RaisePauseStateChanged(group, isPaused);
|
RaisePauseStateChanged(group, isPaused);
|
||||||
@ -508,4 +583,25 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
|
|||||||
protected override void OnInit()
|
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.Abstractions.Query;
|
||||||
using GFramework.Core.Rule;
|
using GFramework.Core.Rule;
|
||||||
|
using GFramework.Cqrs.Abstractions.Cqrs.Query;
|
||||||
|
|
||||||
namespace GFramework.Core.Query;
|
namespace GFramework.Core.Query;
|
||||||
|
|
||||||
@ -24,4 +25,31 @@ public abstract class AbstractAsyncQuery<TResult> : ContextAwareBase, IAsyncQuer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>返回查询结果的异步任务</returns>
|
/// <returns>返回查询结果的异步任务</returns>
|
||||||
protected abstract Task<TResult> OnDoAsync();
|
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.Abstractions.Query;
|
||||||
using GFramework.Core.Rule;
|
using GFramework.Core.Rule;
|
||||||
|
using GFramework.Cqrs.Abstractions.Cqrs.Query;
|
||||||
|
|
||||||
namespace GFramework.Core.Query;
|
namespace GFramework.Core.Query;
|
||||||
|
|
||||||
@ -7,7 +8,7 @@ namespace GFramework.Core.Query;
|
|||||||
/// 抽象查询类,提供查询操作的基础实现
|
/// 抽象查询类,提供查询操作的基础实现
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TResult">查询结果的类型</typeparam>
|
/// <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>
|
/// <summary>
|
||||||
@ -25,4 +26,31 @@ public abstract class AbstractQuery<TResult> : ContextAwareBase, IQuery<TResult>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>查询结果,类型为TResult</returns>
|
/// <returns>查询结果,类型为TResult</returns>
|
||||||
protected abstract TResult OnDo();
|
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);
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
|
||||||
Action<TState>[] listenersSnapshot = Array.Empty<Action<TState>>();
|
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!;
|
TState notificationState = default!;
|
||||||
var hasNotification = false;
|
var hasNotification = false;
|
||||||
var enteredDispatchScope = false;
|
var enteredDispatchScope = false;
|
||||||
@ -224,49 +220,25 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
lock (_lock)
|
var context = EnterDispatchScope(
|
||||||
{
|
action,
|
||||||
EnsureNotDispatching();
|
out var middlewaresSnapshot,
|
||||||
_isDispatching = true;
|
out var reducersSnapshot,
|
||||||
enteredDispatchScope = true;
|
out var stateComparerSnapshot);
|
||||||
context = new StoreDispatchContext<TState>(action!, _state);
|
|
||||||
stateComparerSnapshot = _stateComparer;
|
enteredDispatchScope = true;
|
||||||
middlewaresSnapshot = CreateMiddlewareSnapshotCore();
|
|
||||||
reducersSnapshot = CreateReducerSnapshotCore(context.ActionType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// middleware 和 reducer 可能包含较重的同步逻辑,因此仅持有 dispatch 串行门,
|
// middleware 和 reducer 可能包含较重的同步逻辑,因此仅持有 dispatch 串行门,
|
||||||
// 不占用状态锁,让读取、订阅和注册操作只在需要访问共享状态时短暂阻塞。
|
// 不占用状态锁,让读取、订阅和注册操作只在需要访问共享状态时短暂阻塞。
|
||||||
ExecuteDispatchPipeline(context, middlewaresSnapshot, reducersSnapshot, stateComparerSnapshot);
|
ExecuteDispatchPipeline(context, middlewaresSnapshot, reducersSnapshot, stateComparerSnapshot);
|
||||||
|
|
||||||
lock (_lock)
|
hasNotification = TryCommitDispatchResult(context, out listenersSnapshot, out notificationState);
|
||||||
{
|
|
||||||
_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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (enteredDispatchScope)
|
if (enteredDispatchScope)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
ExitDispatchScope();
|
||||||
{
|
|
||||||
_isDispatching = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -831,6 +803,99 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
context.HasStateChanged = !stateComparer.Equals(context.PreviousState, nextState);
|
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>
|
/// <summary>
|
||||||
/// 确保当前 Store 没有发生重入分发或在 dispatch 中执行历史控制。
|
/// 确保当前 Store 没有发生重入分发或在 dispatch 中执行历史控制。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -949,20 +1014,65 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
|
|
||||||
if (_actionMatchingMode == StoreActionMatchingMode.ExactTypeOnly)
|
if (_actionMatchingMode == StoreActionMatchingMode.ExactTypeOnly)
|
||||||
{
|
{
|
||||||
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
|
return CreateExactReducerSnapshot(actionType);
|
||||||
{
|
|
||||||
return Array.Empty<IStoreReducerAdapter>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
|
|
||||||
for (var i = 0; i < exactReducers.Count; i++)
|
|
||||||
{
|
|
||||||
exactSnapshot[i] = exactReducers[i].Adapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return exactSnapshot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return CreateAssignableReducerSnapshot(actionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为精确类型匹配模式创建 reducer 快照。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="actionType">当前 action 的运行时类型。</param>
|
||||||
|
/// <returns>精确匹配到的 reducer 快照;若未注册则返回空数组。</returns>
|
||||||
|
private IStoreReducerAdapter[] CreateExactReducerSnapshot(Type actionType)
|
||||||
|
{
|
||||||
|
if (!_reducers.TryGetValue(actionType, out var exactReducers) || exactReducers.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<IStoreReducerAdapter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var exactSnapshot = new IStoreReducerAdapter[exactReducers.Count];
|
||||||
|
for (var i = 0; i < exactReducers.Count; i++)
|
||||||
|
{
|
||||||
|
exactSnapshot[i] = exactReducers[i].Adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exactSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为多态匹配模式创建 reducer 快照。
|
||||||
|
/// 该路径会收集所有可赋值的注册桶,并按“精确类型 -> 基类距离 -> 接口 -> 注册顺序”的稳定规则排序。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="actionType">当前 action 的运行时类型。</param>
|
||||||
|
/// <returns>多态模式下的 reducer 快照;若未注册则返回空数组。</returns>
|
||||||
|
private IStoreReducerAdapter[] CreateAssignableReducerSnapshot(Type actionType)
|
||||||
|
{
|
||||||
|
var matches = CollectReducerMatches(actionType);
|
||||||
|
if (matches is null || matches.Count == 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<IStoreReducerAdapter>();
|
||||||
|
}
|
||||||
|
|
||||||
|
matches.Sort(CompareReducerMatch);
|
||||||
|
|
||||||
|
var snapshot = new IStoreReducerAdapter[matches.Count];
|
||||||
|
for (var i = 0; i < matches.Count; i++)
|
||||||
|
{
|
||||||
|
snapshot[i] = matches[i].Adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 收集当前 action 类型可命中的 reducer 注册,并附带稳定排序所需的匹配元数据。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="actionType">当前 action 的运行时类型。</param>
|
||||||
|
/// <returns>匹配结果列表;若没有任何匹配则返回 <see langword="null"/>。</returns>
|
||||||
|
private List<ReducerMatch>? CollectReducerMatches(Type actionType)
|
||||||
|
{
|
||||||
List<ReducerMatch>? matches = null;
|
List<ReducerMatch>? matches = null;
|
||||||
|
|
||||||
foreach (var reducerBucket in _reducers)
|
foreach (var reducerBucket in _reducers)
|
||||||
@ -984,35 +1094,30 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches is null || matches.Count == 0)
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 比较两个 reducer 匹配结果的优先级,保证多态匹配下的执行顺序稳定可预测。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="left">左侧匹配结果。</param>
|
||||||
|
/// <param name="right">右侧匹配结果。</param>
|
||||||
|
/// <returns>排序比较结果。</returns>
|
||||||
|
private static int CompareReducerMatch(ReducerMatch left, ReducerMatch right)
|
||||||
|
{
|
||||||
|
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
|
||||||
|
if (categoryComparison != 0)
|
||||||
{
|
{
|
||||||
return Array.Empty<IStoreReducerAdapter>();
|
return categoryComparison;
|
||||||
}
|
}
|
||||||
|
|
||||||
matches.Sort(static (left, right) =>
|
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
|
||||||
|
if (distanceComparison != 0)
|
||||||
{
|
{
|
||||||
var categoryComparison = left.MatchCategory.CompareTo(right.MatchCategory);
|
return distanceComparison;
|
||||||
if (categoryComparison != 0)
|
|
||||||
{
|
|
||||||
return categoryComparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
var distanceComparison = left.InheritanceDistance.CompareTo(right.InheritanceDistance);
|
|
||||||
if (distanceComparison != 0)
|
|
||||||
{
|
|
||||||
return distanceComparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
return left.Sequence.CompareTo(right.Sequence);
|
|
||||||
});
|
|
||||||
|
|
||||||
var snapshot = new IStoreReducerAdapter[matches.Count];
|
|
||||||
for (var i = 0; i < matches.Count; i++)
|
|
||||||
{
|
|
||||||
snapshot[i] = matches[i].Adapter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return snapshot;
|
return left.Sequence.CompareTo(right.Sequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1412,4 +1517,4 @@ public sealed class Store<TState> : IStore<TState>, IStoreDiagnostics<TState>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TState PendingState { get; set; } = default!;
|
public TState PendingState { get; set; } = default!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using GFramework.Core.Abstractions.Ioc;
|
using GFramework.Core.Abstractions.Ioc;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection.Emit;
|
using System.Reflection.Emit;
|
||||||
|
|
||||||
namespace GFramework.Cqrs.Internal;
|
namespace GFramework.Cqrs.Internal;
|
||||||
@ -88,63 +89,14 @@ internal static class CqrsHandlerRegistrar
|
|||||||
if (registryTypes.Count == 0)
|
if (registryTypes.Count == 0)
|
||||||
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
||||||
|
|
||||||
var registries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
|
if (!TryCreateGeneratedRegistries(registryTypes, assemblyName, logger, out var registries))
|
||||||
foreach (var registryType in registryTypes)
|
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
||||||
{
|
|
||||||
var activationMetadata = RegistryActivationMetadataCache.GetOrAdd(
|
|
||||||
registryType,
|
|
||||||
AnalyzeRegistryActivation);
|
|
||||||
|
|
||||||
if (!activationMetadata.ImplementsRegistryContract)
|
RegisterGeneratedRegistries(services, registries, assemblyName, logger);
|
||||||
{
|
return BuildGeneratedRegistrationResult(
|
||||||
logger.Warn(
|
assemblyMetadata.ReflectionFallbackMetadata,
|
||||||
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
|
assemblyName,
|
||||||
return GeneratedRegistrationResult.NoGeneratedRegistry();
|
logger);
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@ -186,12 +138,138 @@ internal static class CqrsHandlerRegistrar
|
|||||||
// Request/notification handlers receive context injection before every dispatch.
|
// Request/notification handlers receive context injection before every dispatch.
|
||||||
// Transient registration avoids sharing mutable Context across concurrent requests.
|
// Transient registration avoids sharing mutable Context across concurrent requests.
|
||||||
services.AddTransient(handlerInterface, implementationType);
|
services.AddTransient(handlerInterface, implementationType);
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
$"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}.");
|
$"Registered CQRS handler {implementationType.FullName} as {handlerInterface.FullName}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 激活当前程序集声明的所有 generated registry;若任一 registry 不满足运行时契约,则整批回退到反射扫描。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registryTypes">程序集声明的 generated registry 类型列表。</param>
|
||||||
|
/// <param name="assemblyName">用于诊断的程序集稳定名称。</param>
|
||||||
|
/// <param name="logger">日志记录器。</param>
|
||||||
|
/// <param name="registries">成功激活后的 registry 实例。</param>
|
||||||
|
/// <returns>当全部 registry 都可安全激活时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||||
|
private static bool TryCreateGeneratedRegistries(
|
||||||
|
IReadOnlyList<Type> registryTypes,
|
||||||
|
string assemblyName,
|
||||||
|
ILogger logger,
|
||||||
|
out IReadOnlyList<ICqrsHandlerRegistry> registries)
|
||||||
|
{
|
||||||
|
var activatedRegistries = new List<ICqrsHandlerRegistry>(registryTypes.Count);
|
||||||
|
foreach (var registryType in registryTypes)
|
||||||
|
{
|
||||||
|
if (!TryCreateGeneratedRegistry(registryType, assemblyName, logger, out var registry))
|
||||||
|
{
|
||||||
|
registries = Array.Empty<ICqrsHandlerRegistry>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
activatedRegistries.Add(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
registries = activatedRegistries;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 激活单个 generated registry,并在契约不满足时输出与原先完全一致的回退诊断。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registryType">要分析的 generated registry 类型。</param>
|
||||||
|
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||||
|
/// <param name="logger">日志记录器。</param>
|
||||||
|
/// <param name="registry">激活成功后的 registry 实例。</param>
|
||||||
|
/// <returns>当 registry 可安全使用时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||||
|
private static bool TryCreateGeneratedRegistry(
|
||||||
|
Type registryType,
|
||||||
|
string assemblyName,
|
||||||
|
ILogger logger,
|
||||||
|
[NotNullWhen(true)] out ICqrsHandlerRegistry? registry)
|
||||||
|
{
|
||||||
|
var activationMetadata = RegistryActivationMetadataCache.GetOrAdd(
|
||||||
|
registryType,
|
||||||
|
AnalyzeRegistryActivation);
|
||||||
|
|
||||||
|
if (!activationMetadata.ImplementsRegistryContract)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not implement {typeof(ICqrsHandlerRegistry).FullName}.");
|
||||||
|
registry = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activationMetadata.IsAbstract)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it is abstract.");
|
||||||
|
registry = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activationMetadata.Factory is null)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS handler registry {registryType.FullName} in assembly {assemblyName} because it does not expose an accessible parameterless constructor.");
|
||||||
|
registry = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
registry = activationMetadata.Factory();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 调用所有已激活的 generated registry 完成 CQRS handler 注册,并保留稳定的调试日志顺序。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">目标服务集合。</param>
|
||||||
|
/// <param name="registries">已通过契约校验的 registry 实例。</param>
|
||||||
|
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||||
|
/// <param name="logger">日志记录器。</param>
|
||||||
|
private static void RegisterGeneratedRegistries(
|
||||||
|
IServiceCollection services,
|
||||||
|
IReadOnlyList<ICqrsHandlerRegistry> registries,
|
||||||
|
string assemblyName,
|
||||||
|
ILogger logger)
|
||||||
|
{
|
||||||
|
foreach (var registry in registries)
|
||||||
|
{
|
||||||
|
logger.Debug(
|
||||||
|
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
|
||||||
|
registry.Register(services, logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reflectionFallbackMetadata">生成注册器声明的反射补扫元数据。</param>
|
||||||
|
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||||
|
/// <param name="logger">日志记录器。</param>
|
||||||
|
/// <returns>描述 generated registry 是否已完全处理当前程序集的结果对象。</returns>
|
||||||
|
private static GeneratedRegistrationResult BuildGeneratedRegistrationResult(
|
||||||
|
ReflectionFallbackMetadata? reflectionFallbackMetadata,
|
||||||
|
string assemblyName,
|
||||||
|
ILogger logger)
|
||||||
|
{
|
||||||
|
if (reflectionFallbackMetadata is null)
|
||||||
|
return GeneratedRegistrationResult.FullyHandled();
|
||||||
|
|
||||||
|
if (reflectionFallbackMetadata.HasExplicitTypes)
|
||||||
|
{
|
||||||
|
logger.Debug(
|
||||||
|
$"Generated CQRS registry for assembly {assemblyName} requested targeted reflection fallback for {reflectionFallbackMetadata.Types.Count} unsupported handler type(s).");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.Debug(
|
||||||
|
$"Generated CQRS registry for assembly {assemblyName} requested full reflection fallback for unsupported handlers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return GeneratedRegistrationResult.WithReflectionFallback(reflectionFallbackMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取指定实现类型上所有受支持的 CQRS handler 接口,并缓存筛选与排序结果。
|
/// 获取指定实现类型上所有受支持的 CQRS handler 接口,并缓存筛选与排序结果。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -255,6 +333,29 @@ internal static class CqrsHandlerRegistrar
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var resolvedTypes = new List<Type>();
|
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
|
foreach (var fallbackType in fallbackAttributes
|
||||||
.SelectMany(static attribute => attribute.FallbackHandlerTypes)
|
.SelectMany(static attribute => attribute.FallbackHandlerTypes)
|
||||||
.Where(static type => type is not null)
|
.Where(static type => type is not null)
|
||||||
@ -273,37 +374,65 @@ internal static class CqrsHandlerRegistrar
|
|||||||
|
|
||||||
resolvedTypes.Add(fallbackType);
|
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
|
foreach (var typeName in fallbackAttributes
|
||||||
.SelectMany(static attribute => attribute.FallbackHandlerTypeNames)
|
.SelectMany(static attribute => attribute.FallbackHandlerTypeNames)
|
||||||
.Where(static name => !string.IsNullOrWhiteSpace(name))
|
.Where(static name => !string.IsNullOrWhiteSpace(name))
|
||||||
.Distinct(StringComparer.Ordinal)
|
.Distinct(StringComparer.Ordinal)
|
||||||
.OrderBy(static name => name, StringComparer.Ordinal))
|
.OrderBy(static name => name, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
try
|
TryAppendNamedFallbackType(assembly, resolvedTypes, assemblyName, typeName, logger);
|
||||||
{
|
}
|
||||||
var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false);
|
}
|
||||||
if (type is null)
|
|
||||||
{
|
|
||||||
logger.Warn(
|
|
||||||
$"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedTypes.Add(type);
|
/// <summary>
|
||||||
}
|
/// 解析并追加单个按名称声明的 fallback 类型,同时保留“找不到”与“加载异常”两类不同日志语义。
|
||||||
catch (Exception exception)
|
/// </summary>
|
||||||
|
/// <param name="assembly">当前待解析的程序集。</param>
|
||||||
|
/// <param name="resolvedTypes">待补充的已解析类型集合。</param>
|
||||||
|
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||||
|
/// <param name="typeName">要解析的完整类型名。</param>
|
||||||
|
/// <param name="logger">日志记录器。</param>
|
||||||
|
private static void TryAppendNamedFallbackType(
|
||||||
|
Assembly assembly,
|
||||||
|
ICollection<Type> resolvedTypes,
|
||||||
|
string assemblyName,
|
||||||
|
string typeName,
|
||||||
|
ILogger logger)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var type = assembly.GetType(typeName, throwOnError: false, ignoreCase: false);
|
||||||
|
if (type is null)
|
||||||
{
|
{
|
||||||
logger.Warn(
|
logger.Warn(
|
||||||
$"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}");
|
$"Generated CQRS reflection fallback type {typeName} could not be resolved in assembly {assemblyName}. Skipping targeted fallback entry.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return new ReflectionFallbackMetadata(
|
resolvedTypes.Add(type);
|
||||||
resolvedTypes
|
}
|
||||||
.Distinct()
|
catch (Exception exception)
|
||||||
.OrderBy(GetTypeSortKey, StringComparer.Ordinal)
|
{
|
||||||
.ToArray());
|
logger.Warn(
|
||||||
|
$"Generated CQRS reflection fallback type {typeName} failed to load in assembly {assemblyName}: {exception.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -7,33 +7,62 @@
|
|||||||
|
|
||||||
## 当前恢复点
|
## 当前恢复点
|
||||||
|
|
||||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-001`
|
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-012`
|
||||||
- 当前阶段:`Phase 1`
|
- 当前阶段:`Phase 12`
|
||||||
- 当前焦点:
|
- 当前焦点:
|
||||||
- 已将旧 `local-plan/` 迁入 `ai-plan/public/analyzer-warning-reduction/`,active 入口只保留当前恢复信息
|
- 当前 PR review workflow 已补强到支持 JSON 落盘与按 section/path 收窄输出;下一轮恢复到 `MA0046` 主批次
|
||||||
- 基于现有剩余热点,评估 `MA0051`、`MA0048`、`MA0046` 与少量 `MA0016` 是否适合继续在同一主线上处理
|
- 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进
|
||||||
- 若继续推进,优先选择不引入 API rename、公共契约漂移或 Godot 宿主不稳定测试的切入点
|
- 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015` 与 `MA0077`
|
||||||
|
只是当前最明显的低数量示例,不构成限定
|
||||||
|
- 单次 `boot` 的工作树改动上限控制在约 `100` 个文件以内,避免 recovery context 与 review 面同时失控
|
||||||
|
- 若任务边界互不冲突,允许使用不同模型的 subagent 并行处理不同 warning 类型或不同目录,但必须遵守显式 ownership
|
||||||
|
|
||||||
## 当前状态摘要
|
## 当前状态摘要
|
||||||
|
|
||||||
- 已完成 `GFramework.Core`、`GFramework.Cqrs`、`GFramework.Godot` 与部分 source generator 的低风险 warning 清理
|
- 已完成 `GFramework.Core`、`GFramework.Cqrs`、`GFramework.Godot` 与部分 source generator 的低风险 warning 清理
|
||||||
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
|
- 已完成多轮 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 是否继续推进尚未决策
|
- 当前主题仍是 active topic,因为剩余结构性 warning 是否继续推进尚未决策
|
||||||
- `RP-001` 的详细实现历史、测试记录和 warning 热点清单已归档到主题内 `archive/`
|
- `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 映射
|
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
|
||||||
|
|
||||||
## 当前风险
|
## 当前风险
|
||||||
|
|
||||||
- 结构性重构风险:剩余 `MA0051` 与 `MA0048` 可能要求较大的文件拆分或类型重命名
|
- 公共契约兼容风险:剩余 `MA0046` / `MA0016` 若直接改公开委托或集合类型,可能波及用户代码
|
||||||
- 缓解措施:只在下一轮明确接受结构调整成本时再继续推进,不在恢复点模糊的情况下顺手扩面
|
- 缓解措施:优先选择不改公共 API 的低风险切法;若必须触达公共契约,先补齐 XML 契约说明与定向测试
|
||||||
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
|
- 测试宿主稳定性风险:部分 Godot 失败路径在当前 .NET 测试宿主下仍不稳定
|
||||||
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
|
- 缓解措施:继续优先使用稳定的 targeted test、项目构建和相邻 smoke test 组合验证
|
||||||
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
|
- 多目标框架 warning 解释风险:同一源位置会在多个 target framework 下重复计数
|
||||||
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
|
- 缓解措施:继续以唯一源位置和 warning 家族为主要决策依据,而不是只看原始 warning 总数
|
||||||
|
- 并行实现风险:批量收敛时若 subagent 写入边界不清晰,容易引入命名冲突或重复重构
|
||||||
|
- 缓解措施:只在 warning 类型或目录边界清晰时并行;每个 subagent 必须有独占文件 ownership,主代理负责合并验证
|
||||||
|
|
||||||
## 活跃文档
|
## 活跃文档
|
||||||
|
|
||||||
@ -43,10 +72,60 @@
|
|||||||
## 验证说明
|
## 验证说明
|
||||||
|
|
||||||
- `RP-001` 的详细 warning 清理、回归修复与定向验证命令均已迁入主题内历史归档
|
- `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 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
|
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录
|
1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录
|
||||||
2. 从 `MA0051`、`MA0048`、`MA0046` 中只选一个结构性切入点继续,不要在同一轮同时扩多个风险面
|
2. 下一轮优先以 `MA0046` 为主批次启动,先从 `Architecture*` 与 `CoroutineScheduler` 的低风险 delegate 形状修正中选一个切入点
|
||||||
3. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
|
3. 若 `MA0046` 的文件 ownership 可以清晰切分,允许使用不同模型的 subagent 并行处理互不冲突的目录或类型簇
|
||||||
|
4. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
|
||||||
|
|||||||
@ -1,5 +1,275 @@
|
|||||||
# Analyzer Warning Reduction 追踪
|
# 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
|
## 2026-04-19
|
||||||
|
|
||||||
### 阶段:local-plan 迁移收口(RP-001)
|
### 阶段:local-plan 迁移收口(RP-001)
|
||||||
@ -28,5 +298,5 @@
|
|||||||
|
|
||||||
### 下一步
|
### 下一步
|
||||||
|
|
||||||
1. 后续若继续 analyzer warning reduction,只从 `ai-plan/public/analyzer-warning-reduction/` 进入,不再恢复 `local-plan/`
|
1. 若继续 analyzer warning reduction,优先回到 `GFramework.Core` 剩余 `MA0051` 热点,并继续保持“单 warning family、单切入点”的节奏
|
||||||
2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/`
|
2. 后续所有 WSL 下的 .NET 定向验证命令继续显式附带 `-p:RestoreFallbackFolders=`,避免把环境问题误判成代码回归
|
||||||
|
|||||||
@ -7,21 +7,21 @@
|
|||||||
|
|
||||||
## 当前恢复点
|
## 当前恢复点
|
||||||
|
|
||||||
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-003`
|
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-007`
|
||||||
- 当前阶段:`Phase 3`
|
- 当前阶段:`Phase 3`
|
||||||
- 当前焦点:
|
- 当前焦点:
|
||||||
- 已完成 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md` 与
|
- 已完成 `docs/zh-CN/core/events.md`、`property.md` 与 `logging.md` 的专题页重写
|
||||||
`cqrs.md` 的专题页重写
|
- 已按源码与测试复核 `docs/zh-CN/core/state-management.md`、`coroutine.md`,当前内容与实现基本一致,无需再做
|
||||||
- `core` 关键专题页已改回当前 `Architecture`、`ArchitectureContext`、旧 Command/Query 兼容层与新 CQRS
|
机械改写
|
||||||
runtime 的真实入口语义
|
- 已完成 `docs/zh-CN/game/scene.md` 与 `ui.md` 的专题页重写,当前内容已回到“项目自接 factory/root + router 基类”的真实边界
|
||||||
- 下一轮需要继续推进 `docs/zh-CN/core/*` 余下专题页,以及 `docs/zh-CN/game/*`、
|
- 已完成 `docs/zh-CN/source-generators/context-aware-generator.md` 与 `priority-generator.md` 的专题页重写,当前内容已回到“真实生成成员、推荐 API 与兼容边界”的结构
|
||||||
`docs/zh-CN/source-generators/*` 的专题页核对
|
- 下一轮需要把重心转到 Godot 相关生成器页面核对
|
||||||
|
|
||||||
## 当前状态摘要
|
## 当前状态摘要
|
||||||
|
|
||||||
- 文档治理规则已收口到仓库规范,README、站点入口与采用链路不再依赖旧文档自证
|
- 文档治理规则已收口到仓库规范,README、站点入口与采用链路不再依赖旧文档自证
|
||||||
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态
|
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态,本轮计划中的 `core` 剩余高风险页面已完成收口
|
||||||
- 当前主题仍是 active topic,因为 `core` 其余专题页及 `game`、`source-generators` 栏目下仍可能包含与实现漂移的旧内容
|
- 当前主题仍是 active topic,因为 `source-generators` 栏目下的 Godot 相关页面仍可能包含与实现漂移的旧内容
|
||||||
|
|
||||||
## 当前活跃事实
|
## 当前活跃事实
|
||||||
|
|
||||||
@ -32,16 +32,38 @@
|
|||||||
- `docs` 站点构建已验证通过,修正了 VitePress 对 `docs/` 目录外相对链接的 dead-link 检查问题
|
- `docs` 站点构建已验证通过,修正了 VitePress 对 `docs/` 目录外相对链接的 dead-link 检查问题
|
||||||
- `core` 关键专题页已移除 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` 赋值式示例和已移除的
|
- `core` 关键专题页已移除 `Init()`、属性式 `CommandBus` / `QueryBus`、旧 `Input` 赋值式示例和已移除的
|
||||||
`RegisterMediatorBehavior` 等过时说明
|
`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/*` 中仍可能保留看似合理但与
|
- 旧专题页示例失真风险:`docs/zh-CN/game/*` 与 `source-generators/*` 中仍可能保留看似合理但与真实实现不一致的示例
|
||||||
真实实现不一致的示例
|
- 缓解措施:`game/scene.md`、`ui.md`、`source-generators/context-aware-generator.md` 与 `priority-generator.md` 已完成收口;
|
||||||
- 缓解措施:继续按源码、测试、`*.csproj` 与 `ai-libs/` 下已验证参考实现核对,不把旧文档当事实来源
|
继续按源码、测试、`*.csproj` 与 `ai-libs/` 下已验证参考实现核对剩余 Godot 相关页面,不把旧文档当事实来源
|
||||||
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
|
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
|
||||||
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
|
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
|
||||||
- Active 入口回膨胀风险:后续若把栏目级重写过程直接追加到 active 文档,会再次拖慢恢复
|
- Active 入口回膨胀风险:后续若把栏目级重写过程直接追加到 active 文档,会再次拖慢恢复
|
||||||
- 缓解措施:阶段完成并验证后,继续把细节迁入本 topic 的 `archive/`
|
- 缓解措施:阶段完成并验证后,继续把细节迁入本 topic 的 `archive/`
|
||||||
|
- review 跟进遗漏风险:如果 PR review 抓取继续优先选中空 review body,会漏掉 CodeRabbit 的 Nitpick 和
|
||||||
|
linter 跟进项
|
||||||
|
- 缓解措施:保持当前“最新提交 + 最新非空 CodeRabbit review body”解析策略,并在有疑点时以 API 实抓结果复核
|
||||||
|
|
||||||
## 活跃文档
|
## 活跃文档
|
||||||
|
|
||||||
@ -53,10 +75,11 @@
|
|||||||
- 旧 `local-plan/` 的详细实施历史与文档站构建结果已迁入主题内归档
|
- 旧 `local-plan/` 的详细实施历史与文档站构建结果已迁入主题内归档
|
||||||
- active 跟踪文件已按 `ai-plan` 治理规则精简为当前恢复入口
|
- active 跟踪文件已按 `ai-plan` 治理规则精简为当前恢复入口
|
||||||
- `cd docs && bun run build`
|
- `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`
|
1. 继续核对 Godot 相关生成器页面,优先处理 `godot-project-generator.md`、`get-node-generator.md` 与
|
||||||
与 `logging`
|
`bind-node-signal-generator.md`
|
||||||
2. 再推进 `docs/zh-CN/game/*` 与 `docs/zh-CN/source-generators/*` 的专题页重写,优先处理仍引用旧安装方式或旧 API 的页面
|
2. 重点确认 `project.godot`、`AutoLoad` / `InputActions`、`GetNode` / `BindNodeSignal` 示例仍与当前包关系和生成器入口一致
|
||||||
3. 若专题页批量重写完成且验证通过,将本轮 `core` 专题页收口和后续修订过程迁入本 topic 的 `archive/`
|
3. 若 active trace 再积累新的已完成阶段,按恢复点粒度迁入 `archive/traces/`,避免默认启动入口再次膨胀
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
- 历史 trace 归档:
|
- 历史 trace 归档:
|
||||||
- `ai-plan/public/documentation-governance-and-refresh/archive/traces/documentation-governance-and-refresh-history-through-2026-04-18.md`
|
- `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/`
|
1. 后续继续该主题时,只从 `ai-plan/public/documentation-governance-and-refresh/` 进入,不再恢复 `local-plan/`
|
||||||
2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/`
|
2. 若 active 入口再次积累多轮已完成且已验证阶段,继续按同一模式迁入该主题自己的 `archive/`
|
||||||
@ -47,7 +47,7 @@
|
|||||||
- 当前默认导航入口已显著收敛,但专题页仍需逐页按源码与测试继续核对
|
- 当前默认导航入口已显著收敛,但专题页仍需逐页按源码与测试继续核对
|
||||||
- 后续优先级应从 `core` 专题页开始,再向 `game` 与 `source-generators` 扩展
|
- 后续优先级应从 `core` 专题页开始,再向 `game` 与 `source-generators` 扩展
|
||||||
|
|
||||||
### 下一步
|
### 下一步(RP-002)
|
||||||
|
|
||||||
1. 审核 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md`、`cqrs.md`
|
1. 审核 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md`、`cqrs.md`
|
||||||
2. 记录每页的失真点、真实 API 名称与应保留的最小示例
|
2. 记录每页的失真点、真实 API 名称与应保留的最小示例
|
||||||
@ -77,8 +77,107 @@
|
|||||||
是公开入口
|
是公开入口
|
||||||
- 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页重写没有破坏文档站构建
|
- 执行 `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`
|
1. 继续处理 `docs/zh-CN/core/events.md`、`property.md`、`state-management.md`、`coroutine.md`、`logging.md`
|
||||||
2. 保持同样的证据顺序:源码、`*.csproj`、模块 README、`ai-libs/` 参考实现
|
2. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节
|
||||||
3. 完成下一批专题页重写后再次执行 `cd docs && bun run build`
|
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 架构。
|
```bash
|
||||||
|
dotnet add package GeWuYou.GFramework.Core
|
||||||
## 核心接口
|
dotnet add package GeWuYou.GFramework.Core.Abstractions
|
||||||
|
|
||||||
### IEvent
|
|
||||||
|
|
||||||
基础事件接口,定义了事件注册的基本功能。
|
|
||||||
|
|
||||||
**核心方法:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
IUnRegister Register(Action onEvent); // 注册事件处理函数
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### IUnRegister
|
事件实现位于 `GFramework.Core`,抽象接口位于 `GFramework.Core.Abstractions`。
|
||||||
|
|
||||||
注销接口,用于取消事件注册。
|
## 最常用入口
|
||||||
|
|
||||||
**核心方法:**
|
如果你已经在 `ArchitectureContext` 或任何 `IContextAware` 对象里,最常见的入口仍然是:
|
||||||
|
|
||||||
|
- `SendEvent<TEvent>()`
|
||||||
|
- `SendEvent(eventData)`
|
||||||
|
- `RegisterEvent(Action<TEvent>)`
|
||||||
|
- `UnRegisterEvent(Action<TEvent>)`
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
void UnRegister(); // 执行注销操作
|
using GFramework.Core.Extensions;
|
||||||
```
|
using GFramework.Core.System;
|
||||||
|
|
||||||
### IUnRegisterList
|
public sealed record PlayerDiedEvent(int PlayerId);
|
||||||
|
|
||||||
注销列表接口,用于批量管理注销对象。
|
public sealed class CombatSystem : AbstractSystem
|
||||||
|
|
||||||
**属性:**
|
|
||||||
|
|
||||||
```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(() =>
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("Button clicked!");
|
protected override void OnInit()
|
||||||
});
|
{
|
||||||
|
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
|
||||||
|
}
|
||||||
|
|
||||||
// 触发事件
|
private void OnPlayerDied(PlayerDiedEvent @event)
|
||||||
onClicked.Trigger();
|
{
|
||||||
|
Logger.Info("Player died: {0}", @event.PlayerId);
|
||||||
|
}
|
||||||
|
|
||||||
// 取消注册
|
public void KillPlayer(int playerId)
|
||||||
unregister.UnRegister();
|
{
|
||||||
|
this.SendEvent(new PlayerDiedEvent(playerId));
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Event`<T>`
|
如果你在架构外单独使用,也可以直接构造 `EventBus`。
|
||||||
|
|
||||||
单参数泛型事件类,支持一个参数的事件。
|
## EventBus 与 EnhancedEventBus
|
||||||
|
|
||||||
**核心方法:**
|
默认实现是 `EventBus`,提供类型化发送与订阅:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
IUnRegister Register(Action<T> onEvent); // 注册事件监听器
|
using GFramework.Core.Events;
|
||||||
void Trigger(T eventData); // 触发事件并传递参数
|
|
||||||
```
|
|
||||||
|
|
||||||
**使用示例:**
|
|
||||||
|
|
||||||
```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();
|
var eventBus = new EventBus();
|
||||||
|
|
||||||
// 注册类型化事件
|
eventBus.Register<PlayerJoinedEvent>(e =>
|
||||||
eventBus.Register<PlayerDiedEvent>(e =>
|
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Player died at position: {e.Position}");
|
Console.WriteLine(e.Name);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 发送事件(传递实例)
|
eventBus.Send(new PlayerJoinedEvent("Alice"));
|
||||||
eventBus.Send(new PlayerDiedEvent
|
|
||||||
{
|
|
||||||
Position = new Vector3(10, 0, 5)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 发送事件(自动创建实例)
|
|
||||||
eventBus.Send<PlayerDiedEvent>();
|
|
||||||
|
|
||||||
// 注销事件监听器
|
|
||||||
eventBus.UnRegister<PlayerDiedEvent>(OnPlayerDied);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### DefaultUnRegister
|
如果你还需要统计、过滤或弱引用订阅,可以改用 `EnhancedEventBus`。它在 `EventBus` 基础上额外提供:
|
||||||
|
|
||||||
默认注销器实现,封装注销回调。
|
- `Statistics`
|
||||||
|
- `SendFilterable(...)` / `RegisterFilterable(...)`
|
||||||
|
- `SendWeak(...)` / `RegisterWeak(...)`
|
||||||
|
|
||||||
**使用示例:**
|
这类能力更适合工具层、编辑器层或长生命周期对象,不必默认扩散到每个业务事件。
|
||||||
|
|
||||||
|
## 优先级、传播与上下文事件
|
||||||
|
|
||||||
|
当事件处理顺序或“是否继续传播”本身就是语义的一部分时,使用优先级入口:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
Action onUnregister = () => Console.WriteLine("Unregistered");
|
using GFramework.Core.Abstractions.Events;
|
||||||
var unregister = new DefaultUnRegister(onUnregister);
|
using GFramework.Core.Events;
|
||||||
|
|
||||||
// 执行注销
|
public sealed record InputCommand(string Name);
|
||||||
unregister.UnRegister();
|
|
||||||
```
|
|
||||||
|
|
||||||
### OrEvent
|
var eventBus = new EventBus();
|
||||||
|
|
||||||
事件或运算组合器,当任意一个事件触发时触发。
|
eventBus.RegisterWithContext<InputCommand>(ctx =>
|
||||||
|
|
||||||
**核心方法:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
OrEvent Or(IEvent @event); // 添加要组合的事件
|
|
||||||
```
|
|
||||||
|
|
||||||
**使用示例:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var onAnyInput = new OrEvent()
|
|
||||||
.Or(onKeyPressed)
|
|
||||||
.Or(onMouseClicked)
|
|
||||||
.Or(onTouchDetected);
|
|
||||||
|
|
||||||
// 当上述任意事件触发时,执行回调
|
|
||||||
onAnyInput.Register(() =>
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("Input detected!");
|
if (ctx.Data.Name == "Pause")
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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()
|
|
||||||
{
|
{
|
||||||
// 监听生命值变化
|
Console.WriteLine("Pause handled");
|
||||||
Health.Register(newHealth =>
|
ctx.MarkAsHandled();
|
||||||
{
|
|
||||||
if (newHealth <= 0)
|
|
||||||
{
|
|
||||||
// 发送玩家死亡事件
|
|
||||||
this.SendEvent(new PlayerDiedEvent
|
|
||||||
{
|
|
||||||
Position = Position,
|
|
||||||
Cause = "Health depleted"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}, priority: 10);
|
||||||
|
|
||||||
|
eventBus.Send(new InputCommand("Pause"), EventPropagation.UntilHandled);
|
||||||
```
|
```
|
||||||
|
|
||||||
### System 中发送事件
|
当前公开语义是:
|
||||||
|
|
||||||
```csharp
|
- `Register<T>(handler, priority)`:按优先级订阅
|
||||||
public class CombatSystem : AbstractSystem
|
- `RegisterWithContext<T>(...)`:拿到 `EventContext<T>`
|
||||||
{
|
- `EventPropagation.All`:广播给全部监听器
|
||||||
protected override void OnInit() { }
|
- `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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Controller 中注册事件
|
## 局部事件对象
|
||||||
|
|
||||||
```csharp
|
如果事件只在一个对象或一个小模块内部流动,不必一定挂到 `EventBus`。当前仍可直接使用:
|
||||||
using GFramework.Core.Abstractions.Controller;
|
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
|
||||||
|
|
||||||
[ContextAware]
|
- `EasyEvent`
|
||||||
public partial class GameController : IController
|
- `Event<T>`
|
||||||
{
|
- `Event<T1, T2>`
|
||||||
private IUnRegisterList _unregisterList = new UnRegisterList();
|
- `OrEvent`
|
||||||
|
- `EventListenerScope<TEvent>`
|
||||||
|
|
||||||
public void Initialize()
|
这类类型更适合局部组合和 UI/工具层内聚逻辑,不适合作为全局消息总线的替代品。
|
||||||
{
|
|
||||||
// 注册多个事件
|
|
||||||
this.RegisterEvent<GameStartedEvent>(OnGameStarted)
|
|
||||||
.AddToUnregisterList(_unregisterList);
|
|
||||||
|
|
||||||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied)
|
## 与 Store / CQRS 的边界
|
||||||
.AddToUnregisterList(_unregisterList);
|
|
||||||
|
|
||||||
this.RegisterEvent<LevelCompletedEvent>(OnLevelCompleted)
|
- 轻量运行时广播:`EventBus`
|
||||||
.AddToUnregisterList(_unregisterList);
|
- 聚合状态演进:`Store<TState>`,必要时用 `BridgeToEventBus(...)` 兼容旧事件消费者
|
||||||
}
|
- 新业务请求模型:`GFramework.Cqrs`
|
||||||
|
|
||||||
private void OnGameStarted(GameStartedEvent e)
|
一个简单判断规则是:如果你关心“谁来处理、是否有返回值、是否要挂 pipeline”,用 CQRS;如果你只是广播
|
||||||
{
|
“这件事发生了”,事件系统更直接。
|
||||||
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
|
|
||||||
|
|||||||
@ -88,7 +88,7 @@ public sealed class CounterArchitecture : Architecture
|
|||||||
- 要接入游戏内容配置、设置、数据仓库、Scene 或 UI:
|
- 要接入游戏内容配置、设置、数据仓库、Scene 或 UI:
|
||||||
- 转到 [Game](../game/index.md)
|
- 转到 [Game](../game/index.md)
|
||||||
- 要接入 Godot 节点、场景和项目元数据生成:
|
- 要接入 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
|
```csharp
|
||||||
// 日志级别检查
|
using GFramework.Core.Abstractions.Logging;
|
||||||
bool IsTraceEnabled();
|
|
||||||
bool IsDebugEnabled();
|
|
||||||
bool IsInfoEnabled();
|
|
||||||
bool IsWarnEnabled();
|
|
||||||
bool IsErrorEnabled();
|
|
||||||
bool IsFatalEnabled();
|
|
||||||
|
|
||||||
// 记录日志
|
var logger = LoggerFactoryResolver.Provider.CreateLogger("Bootstrap");
|
||||||
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);
|
|
||||||
|
|
||||||
void Debug(string msg);
|
logger.Info("Application started");
|
||||||
void Debug(string format, object arg);
|
logger.Warn("Config file missing");
|
||||||
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();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### ILoggerFactory
|
默认 `ArchitectureConfiguration` 会把 provider 配成 `ConsoleLoggerFactoryProvider`,最小级别是 `Info`。如果你
|
||||||
|
直接走标准 `Architecture` 启动路径,这条配置会自动生效。
|
||||||
|
|
||||||
日志工厂接口,用于创建日志记录器实例。
|
## 在 Architecture 中调整日志级别
|
||||||
|
|
||||||
**核心方法:**
|
|
||||||
|
|
||||||
```csharp
|
```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
|
var configuration = new ArchitectureConfiguration
|
||||||
|
|
||||||
日志工厂提供程序接口,用于获取日志工厂。
|
|
||||||
|
|
||||||
**核心方法:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
ILoggerFactory GetLoggerFactory();
|
|
||||||
ILogger CreateLogger(string name);
|
|
||||||
```
|
|
||||||
|
|
||||||
### LogLevel
|
|
||||||
|
|
||||||
日志级别枚举。
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public enum LogLevel
|
|
||||||
{
|
{
|
||||||
Trace = 0, // 最详细的跟踪信息
|
LoggerProperties = new LoggerProperties
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
}
|
LoggerFactoryProvider = new ConsoleLoggerFactoryProvider
|
||||||
|
|
||||||
protected override void Write(LogLevel level, string message, Exception? exception)
|
|
||||||
{
|
|
||||||
// 自定义日志输出逻辑
|
|
||||||
var logMessage = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
|
|
||||||
if (exception != null)
|
|
||||||
logMessage += $"\n{exception}";
|
|
||||||
|
|
||||||
Console.WriteLine(logMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ConsoleLogger
|
|
||||||
|
|
||||||
控制台日志记录器实现,支持彩色输出。
|
|
||||||
|
|
||||||
**使用示例:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 创建控制台日志记录器
|
|
||||||
var logger = new ConsoleLogger("MyLogger", LogLevel.Debug);
|
|
||||||
|
|
||||||
// 记录不同级别的日志
|
|
||||||
logger.Info("应用程序启动");
|
|
||||||
logger.Debug("调试信息");
|
|
||||||
logger.Warn("警告信息");
|
|
||||||
logger.Error("错误信息");
|
|
||||||
logger.Fatal("致命错误");
|
|
||||||
```
|
|
||||||
|
|
||||||
**输出格式:**
|
|
||||||
|
|
||||||
```
|
|
||||||
[2025-01-09 01:40:00.000] INFO [MyLogger] 应用程序启动
|
|
||||||
[2025-01-09 01:40:01.000] DEBUG [MyLogger] 调试信息
|
|
||||||
[2025-01-09 01:40:02.000] WARN [MyLogger] 警告信息
|
|
||||||
```
|
|
||||||
|
|
||||||
**日志级别颜色:**
|
|
||||||
|
|
||||||
- **Trace**: 深灰色
|
|
||||||
- **Debug**: 青色
|
|
||||||
- **Info**: 白色
|
|
||||||
- **Warning**: 黄色
|
|
||||||
- **Error**: 红色
|
|
||||||
- **Fatal**: 洋红色
|
|
||||||
|
|
||||||
**构造函数参数:**
|
|
||||||
|
|
||||||
- `name`:日志器名称,默认为 "ROOT"
|
|
||||||
- `minLevel`:最低日志级别,默认为 LogLevel.Info
|
|
||||||
- `writer`:TextWriter 输出流,默认为 Console.Out
|
|
||||||
- `useColors`:是否使用颜色,默认为 true(仅在输出到控制台时生效)
|
|
||||||
|
|
||||||
### ConsoleLoggerFactory
|
|
||||||
|
|
||||||
控制台日志工厂,用于创建控制台日志记录器实例。
|
|
||||||
|
|
||||||
**使用示例:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var factory = new ConsoleLoggerFactory();
|
|
||||||
var logger = factory.GetLogger("MyModule", LogLevel.Debug);
|
|
||||||
logger.Info("日志记录器创建成功");
|
|
||||||
```
|
|
||||||
|
|
||||||
### ConsoleLoggerFactoryProvider
|
|
||||||
|
|
||||||
控制台日志工厂提供程序实现。
|
|
||||||
|
|
||||||
**使用示例:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var provider = new ConsoleLoggerFactoryProvider();
|
|
||||||
provider.MinLevel = LogLevel.Debug; // 设置最低日志级别
|
|
||||||
var logger = provider.CreateLogger("MyApp");
|
|
||||||
logger.Info("应用程序启动");
|
|
||||||
```
|
|
||||||
|
|
||||||
### LoggerFactoryResolver
|
|
||||||
|
|
||||||
日志工厂提供程序解析器,用于管理和提供日志工厂提供程序实例。
|
|
||||||
|
|
||||||
**使用示例:**
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 设置日志工厂提供程序
|
|
||||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
|
||||||
|
|
||||||
// 设置最小日志级别
|
|
||||||
LoggerFactoryResolver.MinLevel = LogLevel.Debug;
|
|
||||||
|
|
||||||
// 获取日志记录器
|
|
||||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("MyApp");
|
|
||||||
logger.Info("应用程序启动");
|
|
||||||
```
|
|
||||||
|
|
||||||
## 在架构中使用日志
|
|
||||||
|
|
||||||
### 1. 在 Architecture 中使用
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class GameArchitecture : Architecture
|
|
||||||
{
|
|
||||||
protected override void Init()
|
|
||||||
{
|
|
||||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture");
|
|
||||||
logger.Info("游戏架构初始化开始");
|
|
||||||
|
|
||||||
RegisterModel(new PlayerModel());
|
|
||||||
RegisterSystem(new GameSystem());
|
|
||||||
|
|
||||||
logger.Info("游戏架构初始化完成");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 在 System 中使用
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class CombatSystem : AbstractSystem
|
|
||||||
{
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
|
|
||||||
logger.Info("战斗系统初始化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDestroy()
|
|
||||||
{
|
|
||||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
|
|
||||||
logger.Info("战斗系统已销毁");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 在 Model 中使用
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class PlayerModel : AbstractModel
|
|
||||||
{
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("PlayerModel");
|
|
||||||
logger.Info("玩家模型初始化完成");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 自定义日志级别
|
|
||||||
|
|
||||||
```
|
|
||||||
public class DebugLogger : AbstractLogger
|
|
||||||
{
|
|
||||||
public DebugLogger() : base("Debug", LogLevel.Debug)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Write(LogLevel level, string message, Exception? exception)
|
|
||||||
{
|
|
||||||
// 只输出调试及更高级别的日志
|
|
||||||
if (level >= LogLevel.Debug)
|
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[{level}] {message}");
|
MinLevel = LogLevel.Debug
|
||||||
if (exception != null)
|
|
||||||
Console.WriteLine(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)
|
||||||
|
{
|
||||||
|
structured.Log(
|
||||||
|
LogLevel.Info,
|
||||||
|
"Player matched",
|
||||||
|
("PlayerId", playerId),
|
||||||
|
("RoomId", roomId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 日志级别说明
|
## 当前仓库内置的常用实现
|
||||||
|
|
||||||
| 级别 | 说明 | 使用场景 |
|
- `ConsoleLoggerFactoryProvider`
|
||||||
|-------------|----------|-------------------|
|
- `ConsoleLoggerFactory`
|
||||||
| **Trace** | 最详细的跟踪信息 | 调试复杂的执行流程,记录函数调用等 |
|
- `CompositeLogger`
|
||||||
| **Debug** | 调试信息 | 开发阶段,记录变量值、流程分支等 |
|
- `LoggingConfigurationLoader`
|
||||||
| **Info** | 一般信息 | 记录重要的业务流程和系统状态 |
|
|
||||||
| **Warning** | 警告信息 | 可能的问题但不中断程序执行 |
|
|
||||||
| **Error** | 错误信息 | 影响功能但不致命的问题 |
|
|
||||||
| **Fatal** | 致命错误 | 导致程序无法继续运行的严重错误 |
|
|
||||||
|
|
||||||
## 最佳实践
|
如果你需要文件输出、rolling file、async appender 或 JSON formatter,可以先用
|
||||||
|
`LoggingConfigurationLoader` 读取 `LoggingConfiguration`,再把自定义 `ILoggerFactoryProvider` 挂到
|
||||||
|
`ArchitectureConfiguration.LoggerProperties.LoggerFactoryProvider` 或 `LoggerFactoryResolver.Provider`。
|
||||||
|
|
||||||
1. **使用合适的日志级别**:
|
## 什么时候该换 provider
|
||||||
- 使用 `Info` 记录重要业务流程
|
|
||||||
- 使用 `Debug` 记录调试信息
|
|
||||||
- 使用 `Warning` 记录异常情况
|
|
||||||
- 使用 `Error` 记录错误但不影响程序运行
|
|
||||||
- 使用 `Fatal` 记录严重错误
|
|
||||||
|
|
||||||
2. **提供上下文信息**:
|
下面这些场景通常不该只靠改 `MinLevel`:
|
||||||
```csharp
|
|
||||||
logger.Info($"用户登录成功: UserId={userId}, UserName={userName}");
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **异常日志记录**:
|
- 需要文件输出、rolling file 或 async appender
|
||||||
```csharp
|
- 需要按 namespace / level 做过滤
|
||||||
try
|
- 需要 JSON 格式日志
|
||||||
{
|
- 需要组合多个 appender
|
||||||
// 业务逻辑
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.Error("数据库操作失败", ex);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **分类使用日志**:
|
这时更合理的做法是保留 `ILogger` 调用面不变,只替换 provider / factory / formatter / appender 组合。
|
||||||
```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
|
|
||||||
|
|||||||
@ -1,477 +1,97 @@
|
|||||||
# Property 包使用说明
|
# Property
|
||||||
|
|
||||||
## 概述
|
`GFramework.Core.Property` 负责字段级响应式值。它最适合“一个字段变化就足以驱动视图或局部业务逻辑”的场景;
|
||||||
|
如果你的状态已经是聚合状态树、需要 reducer / middleware / history,再切到
|
||||||
|
[state-management](./state-management.md)。
|
||||||
|
|
||||||
Property 包提供了可绑定属性(BindableProperty)的实现,支持属性值的监听和响应式编程。这是实现数据绑定和响应式编程的核心组件。
|
## 安装方式
|
||||||
|
|
||||||
BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事件机制实现属性变化的通知。
|
```bash
|
||||||
|
dotnet add package GeWuYou.GFramework.Core
|
||||||
> 对于简单字段和局部 UI 绑定,`BindableProperty<T>` 仍然是首选方案。
|
dotnet add package GeWuYou.GFramework.Core.Abstractions
|
||||||
> 如果你需要统一管理复杂状态树、通过 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);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### IBindableProperty`<T>`
|
## 最常用类型
|
||||||
|
|
||||||
可绑定属性接口,继承自只读接口,增加了修改能力。
|
当前最常见的公开类型是:
|
||||||
|
|
||||||
**核心成员:**
|
- `IReadonlyBindableProperty<T>`
|
||||||
|
- `IBindableProperty<T>`
|
||||||
|
- `BindableProperty<T>`
|
||||||
|
|
||||||
|
一般做法是:内部持有 `BindableProperty<T>`,对外只暴露 `IReadonlyBindableProperty<T>`。
|
||||||
|
|
||||||
|
## 最小示例
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// 可读写的属性值
|
using GFramework.Core.Property;
|
||||||
new T Value { get; set; }
|
using GFramework.Core.Abstractions.Property;
|
||||||
|
using GFramework.Core.Model;
|
||||||
|
|
||||||
// 设置值但不触发事件
|
public sealed class PlayerModel : AbstractModel
|
||||||
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 =>
|
|
||||||
{
|
{
|
||||||
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> Health { get; } = new(100);
|
||||||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
|
||||||
public BindableProperty<Vector3> Position { get; } = new(Vector3.Zero);
|
|
||||||
|
|
||||||
// 只读属性(外部只能读取和监听)
|
|
||||||
public IReadonlyBindableProperty<int> ReadonlyHealth => Health;
|
public IReadonlyBindableProperty<int> ReadonlyHealth => Health;
|
||||||
|
|
||||||
protected override void OnInit()
|
public void Damage(int amount)
|
||||||
{
|
{
|
||||||
// 内部监听属性变化
|
Health.Value = Math.Max(0, Health.Value - 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 在 Controller 中监听
|
监听方式:
|
||||||
|
|
||||||
### UI 数据绑定
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
using GFramework.Core.Abstractions.Controller;
|
var unRegister = playerModel.ReadonlyHealth.RegisterWithInitValue(health =>
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
|
||||||
|
|
||||||
[ContextAware]
|
|
||||||
public partial class PlayerUI : Control, IController
|
|
||||||
{
|
{
|
||||||
[Export] private Label _healthLabel;
|
Console.WriteLine($"Current HP: {health}");
|
||||||
[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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 常见使用模式
|
## 当前公开语义
|
||||||
|
|
||||||
### 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
|
`BindableProperty<T>.Comparer` 是按闭合泛型 `T` 共享的静态比较器,`WithComparer(...)` 本质上会改写这一共享
|
||||||
[ContextAware]
|
比较器。也就是说,多个 `BindableProperty<int>` 实例会观察到同一比较规则;只有当你确定整个 `T` 族都要共享同一
|
||||||
public partial class VolumeSlider : HSlider, IController
|
判等语义时,再去改它。
|
||||||
{
|
|
||||||
private BindableProperty<float> _volumeProperty;
|
|
||||||
|
|
||||||
public override void _Ready()
|
## 什么时候继续用 Property
|
||||||
{
|
|
||||||
_volumeProperty = this.GetModel<SettingsModel>().MasterVolume;
|
|
||||||
|
|
||||||
// Model -> UI
|
下面这些场景仍然优先使用 `BindableProperty<T>`:
|
||||||
_volumeProperty.RegisterWithInitValue(vol => Value = vol)
|
|
||||||
.UnRegisterWhenNodeExitTree(this);
|
|
||||||
|
|
||||||
// UI -> Model
|
- 单个字段变化就能驱动 UI
|
||||||
ValueChanged += newValue => _volumeProperty.Value = (float)newValue;
|
- 状态范围局限在单个 Model 或单个页面
|
||||||
}
|
- 不需要统一的 action / reducer 写入口
|
||||||
}
|
- 不需要撤销/重做、历史快照或中间件
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 计算属性
|
## 什么时候该切到 Store
|
||||||
|
|
||||||
```c#
|
如果状态已经演化为下面这些形态,更适合用 `Store<TState>`:
|
||||||
public class PlayerModel : AbstractModel
|
|
||||||
{
|
|
||||||
public BindableProperty<int> Health { get; } = new(100);
|
|
||||||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
|
||||||
public BindableProperty<float> HealthPercent { get; } = new(1.0f);
|
|
||||||
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
// 自动计算百分比
|
|
||||||
Action updatePercent = () =>
|
|
||||||
{
|
|
||||||
HealthPercent.Value = (float)Health.Value / MaxHealth.Value;
|
|
||||||
};
|
|
||||||
|
|
||||||
Health.Register(_ => updatePercent());
|
|
||||||
MaxHealth.Register(_ => updatePercent());
|
|
||||||
|
|
||||||
updatePercent(); // 初始计算
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 属性验证
|
- 多个字段必须作为一个原子状态一起演进
|
||||||
|
- 多个模块共享同一聚合状态
|
||||||
|
- 需要 reducer / middleware / 历史回放
|
||||||
|
- 需要从整棵状态树中复用局部选择逻辑
|
||||||
|
|
||||||
```c#
|
迁移时不必一次性抛弃旧绑定风格。当前已经提供:
|
||||||
public class PlayerModel : AbstractModel
|
|
||||||
{
|
|
||||||
private BindableProperty<int> _health = new(100);
|
|
||||||
|
|
||||||
public BindableProperty<int> Health
|
|
||||||
{
|
|
||||||
get => _health;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
// 限制范围
|
|
||||||
var clampedValue = Math.Clamp(value.Value, 0, MaxHealth.Value);
|
|
||||||
_health.Value = clampedValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
|
||||||
|
|
||||||
protected override void OnInit() { }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 条件监听
|
- `store.Select(...)`
|
||||||
|
- `store.ToBindableProperty(...)`
|
||||||
|
|
||||||
```c#
|
这意味着你可以先把写路径统一到 `Store<TState>`,再渐进迁移现有 UI 或 Controller 的读取方式。
|
||||||
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
|
|
||||||
|
|||||||
@ -1,658 +1,224 @@
|
|||||||
---
|
---
|
||||||
title: 场景系统
|
title: 场景系统
|
||||||
description: 场景系统提供了完整的场景生命周期管理、路由导航和转换控制功能。
|
description: 说明 GFramework.Game 场景路由的当前入口、项目侧接入职责与扩展边界。
|
||||||
---
|
---
|
||||||
|
|
||||||
# 场景系统
|
# 场景系统
|
||||||
|
|
||||||
## 概述
|
`GFramework.Game` 的场景系统是“路由基类 + 场景契约 + 过渡管线”的组合,不是替你包办注册表、节点树和引擎对象装配的
|
||||||
|
一体化方案。
|
||||||
|
|
||||||
场景系统是 GFramework.Game 中用于管理游戏场景的核心组件。它提供了场景的加载、卸载、切换、暂停和恢复等完整生命周期管理,以及基于栈的场景导航机制。
|
框架当前负责的是:
|
||||||
|
|
||||||
通过场景系统,你可以轻松实现场景之间的平滑切换,管理场景栈(如主菜单 -> 游戏 -> 暂停菜单),并在场景转换时执行自定义逻辑。
|
- 场景栈管理
|
||||||
|
- `Load -> Enter -> Pause -> Resume -> Exit -> Unload` 生命周期顺序
|
||||||
|
- 路由守卫与过渡处理器执行时机
|
||||||
|
- `SceneRouterBase` 这一层的默认切换编排
|
||||||
|
|
||||||
**主要特性**:
|
项目或引擎适配层仍然需要自己提供:
|
||||||
|
|
||||||
- 完整的场景生命周期管理
|
- `ISceneFactory`
|
||||||
- 基于栈的场景导航
|
- `ISceneRoot`
|
||||||
- 场景转换管道和钩子
|
- 具体的 `ISceneBehavior` / `IScene`
|
||||||
- 路由守卫(Route Guard)
|
- 场景键和资源、节点、预制体之间的映射关系
|
||||||
- 场景工厂和行为模式
|
|
||||||
- 异步加载和卸载
|
|
||||||
|
|
||||||
## 核心概念
|
如果你把它理解为“可复用的场景路由底座”而不是“现成的完整场景框架”,后续接法会更贴近源码。
|
||||||
|
|
||||||
### 场景接口
|
## 当前公开入口
|
||||||
|
|
||||||
`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
|
```csharp
|
||||||
public interface IScene
|
using GFramework.Game.Scene;
|
||||||
|
using LoggingTransitionHandler = GFramework.Game.Scene.Handler.LoggingTransitionHandler;
|
||||||
|
|
||||||
|
public sealed class GameSceneRouter : SceneRouterBase
|
||||||
{
|
{
|
||||||
ValueTask OnLoadAsync(ISceneEnterParam? param); // 加载资源
|
protected override void RegisterHandlers()
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
// 加载场景资源
|
RegisterHandler(new LoggingTransitionHandler());
|
||||||
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); // 模拟卸载
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 注册场景
|
这一步只解决“切换流程怎么跑”,不解决“场景从哪来”。
|
||||||
|
|
||||||
在场景注册表中注册场景:
|
### 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
|
```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()
|
||||||
{
|
{
|
||||||
// 注册场景
|
__InjectContextBindings_Generated();
|
||||||
Register("MainMenu", typeof(MainMenuScene));
|
_sceneRouter.BindRoot(this);
|
||||||
Register("Gameplay", typeof(GameplayScene));
|
|
||||||
Register("Pause", typeof(PauseScene));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
```csharp
|
||||||
using GFramework.Core.Abstractions.Controller;
|
await sceneRouter.ReplaceAsync(
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
"Gameplay",
|
||||||
|
new GameplayEnterParam
|
||||||
[ContextAware]
|
|
||||||
public partial class GameController : IController
|
|
||||||
{
|
|
||||||
public async Task StartGame()
|
|
||||||
{
|
{
|
||||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
Seed = "new-game"
|
||||||
|
});
|
||||||
|
|
||||||
// 替换当前场景(清空场景栈)
|
await sceneRouter.PushAsync("PauseMenu");
|
||||||
await sceneRouter.ReplaceAsync("Gameplay");
|
await sceneRouter.PopAsync();
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 高级用法
|
## 扩展点
|
||||||
|
|
||||||
### 场景参数传递
|
|
||||||
|
|
||||||
通过 `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
|
- `CanEnterAsync(string sceneKey, ISceneEnterParam? param)`
|
||||||
using GFramework.Game.Abstractions.Scene;
|
- `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;
|
- `RegisterHandler(ISceneTransitionHandler handler, SceneTransitionHandlerOptions? options = null)`
|
||||||
private async Task SaveGameAsync() => await Task.Delay(100);
|
- `RegisterAroundHandler(ISceneAroundTransitionHandler handler, SceneTransitionHandlerOptions? options = null)`
|
||||||
private bool CheckGameplayRequirements() => true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册守卫
|
适合放:
|
||||||
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)
|
- “框架会帮你直接注册和发现所有场景类型”
|
||||||
{
|
- “只要写一个 `IScene` 就能自动接入所有引擎对象”
|
||||||
Console.WriteLine($"场景加载完成: {@event.ToKey}");
|
- “场景系统本身自带统一注册表和完整项目结构”
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask OnBeforeEnterAsync(SceneTransitionEvent @event)
|
当前更准确的理解是:
|
||||||
{
|
|
||||||
Console.WriteLine($"准备进入场景: {@event.ToKey}");
|
|
||||||
// 播放淡入动画
|
|
||||||
await PlayFadeIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
- 框架提供通用场景切换编排
|
||||||
{
|
- 项目提供 factory、root、资源映射和具体引擎装配
|
||||||
Console.WriteLine($"已进入场景: {@event.ToKey}");
|
- 文档中的最小示例应优先说明职责边界,而不是继续堆叠大而全教程
|
||||||
// 隐藏加载画面
|
|
||||||
await HideLoadingScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event)
|
## 推荐阅读
|
||||||
{
|
|
||||||
Console.WriteLine($"准备退出场景: {@event.FromKey}");
|
|
||||||
// 播放淡出动画
|
|
||||||
await PlayFadeOut();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask OnAfterExitAsync(SceneTransitionEvent @event)
|
1. [game/index.md](./index.md)
|
||||||
{
|
2. [ui.md](./ui.md)
|
||||||
Console.WriteLine($"已退出场景: {@event.FromKey}");
|
3. `GFramework.Game/README.md`
|
||||||
await Task.CompletedTask;
|
4. `GFramework.Game.Abstractions/README.md`
|
||||||
}
|
|
||||||
|
|
||||||
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,509 +1,293 @@
|
|||||||
---
|
---
|
||||||
title: UI 系统
|
title: UI 系统
|
||||||
description: UI 系统提供了完整的 UI 页面管理、路由导航和多层级显示功能。
|
description: 说明 GFramework.Game UI 路由当前的页面栈、层级 UI、输入语义与项目侧接入方式。
|
||||||
---
|
---
|
||||||
|
|
||||||
# UI 系统
|
# UI 系统
|
||||||
|
|
||||||
## 概述
|
`GFramework.Game` 的 UI 系统不是单纯的“页面栈”。按当前实现,它同时覆盖:
|
||||||
|
|
||||||
UI 系统是 GFramework.Game 中用于管理游戏 UI 界面的核心组件。它提供了 UI 页面的生命周期管理、基于栈的导航机制,以及多层级的
|
- `UiLayer.Page` 的页面导航
|
||||||
UI 显示系统(Page、Overlay、Modal、Toast、Topmost)。
|
- `Overlay` / `Modal` / `Toast` / `Topmost` 的层级 UI
|
||||||
|
- UI 语义动作捕获与分发
|
||||||
|
- World 输入阻断
|
||||||
|
- 由 UI 可见性驱动的暂停语义
|
||||||
|
|
||||||
通过 UI 系统,你可以轻松实现 UI 页面之间的切换,管理 UI 栈(如主菜单 -> 设置 -> 关于),以及在不同层级显示各种类型的
|
因此,新的接入文档不应再把它写成“只有 Push/Pop 的传统页面管理器”。
|
||||||
UI(对话框、提示、加载界面等)。
|
|
||||||
|
|
||||||
**主要特性**:
|
## 当前公开入口
|
||||||
|
|
||||||
- 完整的 UI 生命周期管理
|
### `IUiPage`
|
||||||
- 基于栈的 UI 导航
|
|
||||||
- 多层级 UI 显示(5 个层级)
|
|
||||||
- UI 转换管道和钩子
|
|
||||||
- 路由守卫(Route Guard)
|
|
||||||
- UI 工厂和行为模式
|
|
||||||
|
|
||||||
## 核心概念
|
最轻量的页面生命周期契约,暴露:
|
||||||
|
|
||||||
### 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
|
```csharp
|
||||||
public interface IUiPage
|
using GFramework.Game.UI;
|
||||||
|
using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler;
|
||||||
|
|
||||||
|
public sealed class GameUiRouter : UiRouterBase
|
||||||
{
|
{
|
||||||
void OnEnter(IUiPageEnterParam? param); // 进入页面
|
protected override void RegisterHandlers()
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
Console.WriteLine("进入主菜单");
|
RegisterHandler(new LoggingTransitionHandler());
|
||||||
// 初始化 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 元素
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 切换 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
|
```csharp
|
||||||
using GFramework.Core.Abstractions.Controller;
|
architecture.RegisterUtility<IUiFactory>(new GameUiFactory());
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
architecture.RegisterSystem(new GameUiRouter());
|
||||||
|
```
|
||||||
|
|
||||||
[ContextAware]
|
### 5. 在 root 就绪后绑定
|
||||||
public partial class UiController : IController
|
|
||||||
|
```csharp
|
||||||
|
public sealed class UiRoot : CanvasLayer, IUiRoot
|
||||||
{
|
{
|
||||||
public async Task ShowSettings()
|
[GetSystem] private IUiRouter _uiRouter = null!;
|
||||||
{
|
|
||||||
var uiRouter = this.GetSystem<IUiRouter>();
|
|
||||||
|
|
||||||
// 压入设置页面(保留当前页面)
|
public override void _Ready()
|
||||||
await uiRouter.PushAsync("Settings");
|
{
|
||||||
|
__InjectContextBindings_Generated();
|
||||||
|
_uiRouter.BindRoot(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CloseSettings()
|
public void AddUiPage(IUiPageBehavior child)
|
||||||
{
|
{
|
||||||
var uiRouter = this.GetSystem<IUiRouter>();
|
AddUiPage(child, UiLayer.Page);
|
||||||
|
|
||||||
// 弹出当前页面(返回上一页)
|
|
||||||
await uiRouter.PopAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ShowMainMenu()
|
public void AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)
|
||||||
{
|
{
|
||||||
var uiRouter = this.GetSystem<IUiRouter>();
|
// 项目侧决定如何把 child.View 挂到具体容器
|
||||||
|
}
|
||||||
|
|
||||||
// 替换所有页面(清空 UI 栈)
|
public void RemoveUiPage(IUiPageBehavior child)
|
||||||
await uiRouter.ReplaceAsync("MainMenu");
|
{
|
||||||
|
// 项目侧决定如何移除并释放视图
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 显示不同层级的 UI
|
### 6. 从业务代码区分两类入口
|
||||||
|
|
||||||
|
页面栈:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
[ContextAware]
|
await uiRouter.ReplaceAsync("MainMenu");
|
||||||
public partial class UiController : IController
|
await uiRouter.PushAsync("Settings", new SettingsEnterParam());
|
||||||
{
|
await uiRouter.PopAsync(UiPopPolicy.Destroy);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 高级用法
|
层级 UI:
|
||||||
|
|
||||||
### UI 参数传递
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// 定义 UI 参数
|
var modalHandle = uiRouter.Show(
|
||||||
public class SettingsEnterParam : IUiPageEnterParam
|
"ConfirmExit",
|
||||||
{
|
UiLayer.Modal,
|
||||||
public string Category { get; set; }
|
new ConfirmExitParam());
|
||||||
}
|
|
||||||
|
|
||||||
// 在 UI 中接收参数
|
uiRouter.Hide(modalHandle, UiLayer.Modal);
|
||||||
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"
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 扩展点
|
||||||
|
|
||||||
### 路由守卫
|
### 路由守卫
|
||||||
|
|
||||||
```csharp
|
如果你要在进入或离开页面前做业务检查,实现 `IUiRouteGuard`:
|
||||||
using GFramework.Game.Abstractions.UI;
|
|
||||||
|
|
||||||
public class UnsavedChangesGuard : IUiRouteGuard
|
- `CanEnterAsync(string uiKey, IUiPageEnterParam? param)`
|
||||||
{
|
- `CanLeaveAsync(string uiKey)`
|
||||||
public async ValueTask<bool> CanLeaveAsync(
|
|
||||||
IUiPageBehavior from,
|
|
||||||
string toKey,
|
|
||||||
IUiPageEnterParam? param)
|
|
||||||
{
|
|
||||||
// 检查是否有未保存的更改
|
|
||||||
if (from.Key == "Settings" && HasUnsavedChanges())
|
|
||||||
{
|
|
||||||
var confirmed = await ShowConfirmDialog();
|
|
||||||
return confirmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册守卫
|
`IUiRouter` 当前公开的是:
|
||||||
uiRouter.AddGuard(new UnsavedChangesGuard());
|
|
||||||
```
|
|
||||||
|
|
||||||
### UI 转换处理器
|
- `RegisterHandler(IUiTransitionHandler handler, UiTransitionHandlerOptions? options = null)`
|
||||||
|
- `UnregisterHandler(IUiTransitionHandler handler)`
|
||||||
|
|
||||||
```csharp
|
适合放:
|
||||||
using GFramework.Game.Abstractions.UI;
|
|
||||||
|
|
||||||
public class FadeTransitionHandler : IUiTransitionHandler
|
- UI 转场动画
|
||||||
{
|
- 统一日志
|
||||||
public async ValueTask OnBeforeEnterAsync(UiTransitionEvent @event)
|
- 栈变化埋点
|
||||||
{
|
|
||||||
Console.WriteLine($"准备进入 UI: {@event.ToKey}");
|
|
||||||
await PlayFadeIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
1. 设备输入 -> `UiInputAction`
|
||||||
{
|
2. `IUiRouter.TryDispatchUiAction(...)`
|
||||||
Console.WriteLine($"已退出 UI: {@event.FromKey}");
|
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
|
- “所有 UI 都统一通过一个 Show API 管理”
|
||||||
using GFramework.Core.Abstractions.Controller;
|
- “UI 系统只有页面栈,不涉及输入阻断和暂停语义”
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
- “Modal / Topmost 只是视觉层级,不影响交互”
|
||||||
|
|
||||||
[ContextAware]
|
当前更准确的理解是:
|
||||||
public partial class DialogController : IController
|
|
||||||
{
|
|
||||||
private UiHandle? _dialogHandle;
|
|
||||||
|
|
||||||
public void ShowDialog()
|
- 页面栈和层级 UI 是两套入口
|
||||||
{
|
- 页面行为不仅有生命周期,还有输入、阻断、暂停契约
|
||||||
var uiRouter = this.GetSystem<IUiRouter>();
|
- router 是 UI 语义仲裁中心,项目输入层应主动接入它
|
||||||
|
|
||||||
// 显示对话框并保存句柄
|
## 推荐阅读
|
||||||
_dialogHandle = uiRouter.Show("ConfirmDialog", UiLayer.Modal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseDialog()
|
1. [game/index.md](./index.md)
|
||||||
{
|
2. [scene.md](./scene.md)
|
||||||
if (_dialogHandle.HasValue)
|
3. [../source-generators/auto-ui-page-generator.md](../source-generators/auto-ui-page-generator.md)
|
||||||
{
|
4. `GFramework.Game/README.md`
|
||||||
var uiRouter = this.GetSystem<IUiRouter>();
|
5. `GFramework.Game.Abstractions/README.md`
|
||||||
|
|
||||||
// 使用句柄关闭对话框
|
|
||||||
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,403 +1,198 @@
|
|||||||
|
---
|
||||||
|
title: ContextAware 生成器
|
||||||
|
description: 说明 [ContextAware] 当前会生成什么、何时使用、与 ContextAwareBase 的边界以及测试场景。
|
||||||
|
---
|
||||||
|
|
||||||
# ContextAware 生成器
|
# ContextAware 生成器
|
||||||
|
|
||||||
> 自动实现 IContextAware 接口,提供架构上下文访问能力
|
`[ContextAware]` 是 `GFramework.Core.SourceGenerators` 中最常用的一类生成器。它的职责很明确:
|
||||||
|
|
||||||
## 概述
|
- 为当前类型自动补齐 `IContextAware`
|
||||||
|
- 提供可复用的上下文懒加载入口
|
||||||
|
- 让类型可以直接使用 `this.GetSystem<T>()`、`this.GetModel<T>()`、`this.GetUtility<T>()` 等扩展方法
|
||||||
|
|
||||||
ContextAware 生成器为标记了 `[ContextAware]` 属性的类自动生成 `IContextAware` 接口实现,使类能够便捷地访问架构上下文(
|
它不负责注册服务,也不会替你决定应该取哪个 `System` / `Model`。它解决的是“当前类型如何拿到架构上下文”。
|
||||||
`IArchitectureContext`)。这是 GFramework 中最常用的源码生成器之一,几乎所有需要与架构交互的组件都会使用它。
|
|
||||||
|
|
||||||
### 核心功能
|
## 当前包关系
|
||||||
|
|
||||||
- **自动接口实现**:无需手动实现 `IContextAware` 接口的 `SetContext()` 和 `GetContext()` 方法
|
- 特性来源:`GFramework.Core.SourceGenerators.Abstractions`
|
||||||
- **懒加载上下文**:`Context` 属性在首次访问时自动初始化
|
- 生成器实现:`GFramework.Core.SourceGenerators`
|
||||||
- **默认提供者**:使用 `GameContextProvider` 作为默认上下文提供者
|
- 运行时接口:`GFramework.Core.Abstractions.Rule.IContextAware`
|
||||||
- **测试友好**:支持通过 `SetContextProvider()` 配置自定义上下文提供者
|
- 常用扩展方法:`GFramework.Core.Extensions`
|
||||||
|
|
||||||
## 基础使用
|
如果只安装运行时 `GFramework.Core` 而没有安装 `Core.SourceGenerators`,`[ContextAware]` 本身不会生效。
|
||||||
|
|
||||||
### 标记类
|
## 最小用法
|
||||||
|
|
||||||
使用 `[ContextAware]` 属性标记需要访问架构上下文的类:
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
|
||||||
using GFramework.Core.Abstractions.Controller;
|
using GFramework.Core.Abstractions.Controller;
|
||||||
|
using GFramework.Core.Extensions;
|
||||||
|
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
[ContextAware]
|
[ContextAware]
|
||||||
public partial class PlayerController : IController
|
public partial class PlayerController : IController
|
||||||
{
|
{
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
|
var playerModel = this.GetModel<IPlayerModel>();
|
||||||
var playerModel = this.GetModel<PlayerModel>();
|
var combatSystem = this.GetSystem<ICombatSystem>();
|
||||||
var combatSystem = this.GetSystem<CombatSystem>();
|
|
||||||
|
|
||||||
this.SendEvent(new PlayerInitializedEvent());
|
combatSystem.Bind(playerModel);
|
||||||
}
|
|
||||||
|
|
||||||
public void Attack(Enemy target)
|
|
||||||
{
|
|
||||||
var damage = this.GetUtility<DamageCalculator>().Calculate(this, target);
|
|
||||||
this.SendCommand(new DealDamageCommand(target, damage));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 必要条件
|
当前最重要的前置条件只有两个:
|
||||||
|
|
||||||
标记的类必须满足以下条件:
|
- 必须是 `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
|
```csharp
|
||||||
// ✅ 正确
|
((IContextAware)controller).SetContext(context);
|
||||||
[ContextAware]
|
|
||||||
public partial class MyController { }
|
|
||||||
|
|
||||||
// ❌ 错误:缺少 partial 关键字
|
|
||||||
[ContextAware]
|
|
||||||
public class MyController { }
|
|
||||||
|
|
||||||
// ❌ 错误:不能用于 struct
|
|
||||||
[ContextAware]
|
|
||||||
public partial struct MyStruct { }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 生成的代码
|
### 生成路径和 `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
|
```csharp
|
||||||
// <auto-generated/>
|
PlayerController.SetContextProvider(new TestContextProvider(testArchitecture.Context));
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace YourNamespace;
|
try
|
||||||
|
|
||||||
partial class PlayerController : global::GFramework.Core.Abstractions.Rule.IContextAware
|
|
||||||
{
|
{
|
||||||
private global::GFramework.Core.Abstractions.Architecture.IArchitectureContext? _context;
|
var controller = new PlayerController();
|
||||||
private static global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider? _contextProvider;
|
controller.Initialize();
|
||||||
|
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
finally
|
||||||
|
|
||||||
### 代码解析
|
|
||||||
|
|
||||||
生成的代码包含以下关键部分:
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
// 清理:重置上下文提供者
|
|
||||||
PlayerController.ResetContextProvider();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 多架构场景
|
|
||||||
|
|
||||||
在某些高级场景中,可能需要同时运行多个架构实例:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class MultiArchitectureManager
|
|
||||||
{
|
|
||||||
private readonly Dictionary<string, IArchitecture> _architectures = new();
|
|
||||||
|
|
||||||
public void SwitchToArchitecture(string name)
|
|
||||||
{
|
|
||||||
var architecture = _architectures[name];
|
|
||||||
var provider = new ScopedContextProvider(architecture);
|
|
||||||
|
|
||||||
// 为所有使用 [ContextAware] 的类切换上下文
|
|
||||||
PlayerController.SetContextProvider(provider);
|
|
||||||
EnemyController.SetContextProvider(provider);
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用场景
|
|
||||||
|
|
||||||
### 何时使用 [ContextAware]
|
|
||||||
|
|
||||||
推荐在以下场景使用 `[ContextAware]` 属性:
|
|
||||||
|
|
||||||
1. **Controller 层**:需要协调多个 Model/System 的控制器
|
|
||||||
2. **Command/Query 实现**:需要访问架构服务的命令或查询
|
|
||||||
3. **自定义组件**:不继承框架基类但需要上下文访问的组件
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[ContextAware]
|
|
||||||
public partial class GameFlowController : IController
|
|
||||||
{
|
|
||||||
public async Task StartGame()
|
|
||||||
{
|
|
||||||
var saveSystem = this.GetSystem<SaveSystem>();
|
|
||||||
var uiSystem = this.GetSystem<UISystem>();
|
|
||||||
|
|
||||||
await saveSystem.LoadAsync();
|
|
||||||
await uiSystem.ShowMainMenuAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 与 IController 配合使用
|
|
||||||
|
|
||||||
在 Godot 项目中,控制器通常同时实现 `IController` 和使用 `[ContextAware]`:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
using GFramework.Core.Abstractions.Controller;
|
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
|
||||||
|
|
||||||
[ContextAware]
|
|
||||||
public partial class PlayerController : Node, IController
|
|
||||||
{
|
|
||||||
public override void _Ready()
|
|
||||||
{
|
|
||||||
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
|
|
||||||
var playerModel = this.GetModel<PlayerModel>();
|
|
||||||
var combatSystem = this.GetSystem<CombatSystem>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**说明**:
|
|
||||||
|
|
||||||
- `IController` 是标记接口,标识这是一个控制器
|
|
||||||
- `[ContextAware]` 提供架构访问能力
|
|
||||||
- 两者配合使用是推荐的模式
|
|
||||||
|
|
||||||
### 何时继承 ContextAwareBase
|
|
||||||
|
|
||||||
如果类需要更多框架功能(如生命周期管理),应继承 `ContextAwareBase`:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 推荐:需要生命周期管理时继承基类
|
|
||||||
public class PlayerModel : AbstractModel
|
|
||||||
{
|
|
||||||
// AbstractModel 已经继承了 ContextAwareBase
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
var config = this.GetUtility<ConfigLoader>().Load<PlayerConfig>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 推荐:简单组件使用属性
|
|
||||||
[ContextAware]
|
|
||||||
public partial class SimpleHelper
|
|
||||||
{
|
|
||||||
public void DoSomething()
|
|
||||||
{
|
|
||||||
this.SendEvent(new SomethingHappenedEvent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 与 IContextAware 接口的关系
|
|
||||||
|
|
||||||
生成的代码实现了 `IContextAware` 接口:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
namespace GFramework.Core.Abstractions.Rule;
|
|
||||||
|
|
||||||
public interface IContextAware
|
|
||||||
{
|
|
||||||
void SetContext(IArchitectureContext context);
|
|
||||||
IArchitectureContext GetContext();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
这意味着标记了 `[ContextAware]` 的类可以:
|
|
||||||
|
|
||||||
1. **被架构自动注入上下文**:实现 `IContextAware` 的类在注册到架构时会自动调用 `SetContext()`
|
|
||||||
2. **参与依赖注入**:可以作为 `IContextAware` 类型注入到其他组件
|
|
||||||
3. **支持上下文传递**:可以通过 `GetContext()` 将上下文传递给其他组件
|
|
||||||
|
|
||||||
## 最佳实践
|
|
||||||
|
|
||||||
### 1. 始终使用 partial 关键字
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// ✅ 正确
|
|
||||||
[ContextAware]
|
|
||||||
public partial class MyController { }
|
|
||||||
|
|
||||||
// ❌ 错误:编译器会报错
|
|
||||||
[ContextAware]
|
|
||||||
public class MyController { }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 在测试中清理上下文提供者
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[TearDown]
|
|
||||||
public void TearDown()
|
|
||||||
{
|
|
||||||
// 避免测试之间的状态污染
|
|
||||||
PlayerController.ResetContextProvider();
|
PlayerController.ResetContextProvider();
|
||||||
EnemyController.ResetContextProvider();
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 避免在构造函数中访问 Context
|
需要注意两点:
|
||||||
|
|
||||||
```csharp
|
- `ResetContextProvider()` 只会重置共享 provider,不会清除已创建实例上的 `_context`
|
||||||
[ContextAware]
|
- 如果测试要复用同一实例并切换上下文,应该显式调用 `((IContextAware)instance).SetContext(...)`
|
||||||
public partial class MyController
|
|
||||||
{
|
|
||||||
// ❌ 错误:构造函数执行时上下文可能未初始化
|
|
||||||
public MyController()
|
|
||||||
{
|
|
||||||
var model = this.GetModel<SomeModel>(); // 可能为 null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 正确:在初始化方法中访问
|
## 诊断与约束
|
||||||
public void Initialize()
|
|
||||||
{
|
|
||||||
var model = this.GetModel<SomeModel>(); // 安全
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 优先使用 Context 属性而非接口方法
|
当前文档里最值得记住的约束只有这些:
|
||||||
|
|
||||||
```csharp
|
- 非 `class` 会触发 `GF_Rule_001`
|
||||||
[ContextAware]
|
- 非 `partial` 不会生成实现,并会触发公共 partial 约束诊断
|
||||||
public partial class MyController
|
- 嵌套、字段注入等其他错误通常由对应的 Context Get 生成器和其诊断补充报告
|
||||||
{
|
|
||||||
public void DoSomething()
|
|
||||||
{
|
|
||||||
// ✅ 推荐:使用扩展方法
|
|
||||||
var model = this.GetModel<SomeModel>();
|
|
||||||
|
|
||||||
// ❌ 不推荐:显式调用接口方法
|
## 与旧写法的边界
|
||||||
var context = ((IContextAware)this).GetContext();
|
|
||||||
var model2 = context.GetModel<SomeModel>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 诊断信息
|
下面这些旧说法已经不够准确:
|
||||||
|
|
||||||
生成器会在以下情况报告编译错误:
|
- “`[ContextAware]` 只是帮你补一个简单的 `GetContext()`”
|
||||||
|
- “切换 provider 后,已有实例会自动跟着切换”
|
||||||
|
- “`[ContextAware]` 和 `ContextAwareBase` 的默认行为完全一致”
|
||||||
|
|
||||||
### GFSG001: 类必须是 partial
|
当前更准确的理解是:
|
||||||
|
|
||||||
```csharp
|
- 生成实现带有实例缓存、类型级共享 provider 和同步锁
|
||||||
[ContextAware]
|
- provider 切换只影响尚未缓存上下文的实例
|
||||||
public class MyController { } // 错误:缺少 partial 关键字
|
- `ContextAwareBase` 是更轻量的实例级缓存路径
|
||||||
```
|
|
||||||
|
|
||||||
**解决方案**:添加 `partial` 关键字
|
## 推荐阅读
|
||||||
|
|
||||||
```csharp
|
1. [context-get-generator.md](./context-get-generator.md)
|
||||||
[ContextAware]
|
2. [logging-generator.md](./logging-generator.md)
|
||||||
public partial class MyController { } // ✅ 正确
|
3. [../core/index.md](../core/index.md)
|
||||||
```
|
4. `GFramework.Core.SourceGenerators/README.md`
|
||||||
|
|
||||||
### 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,250 +1,104 @@
|
|||||||
|
---
|
||||||
|
title: Priority 生成器
|
||||||
|
description: 说明 [Priority] 当前会生成什么、何时生效、应配合哪些优先级 API 使用,以及动态优先级的边界。
|
||||||
|
---
|
||||||
|
|
||||||
# Priority 生成器
|
# 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
|
```csharp
|
||||||
using GFramework.Core.Abstractions.Bases;
|
using GFramework.Core.Abstractions.Bases;
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Bases;
|
using GFramework.Core.SourceGenerators.Abstractions.Bases;
|
||||||
|
|
||||||
[Priority(PriorityGroup.Critical)] // -100
|
[Priority(PriorityGroup.High)]
|
||||||
public partial class InputSystem : AbstractSystem { }
|
public partial class SaveSystem : 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
|
|
||||||
{
|
{
|
||||||
public const int Critical = -100; // 关键:输入、网络等
|
protected override void OnInit()
|
||||||
public const int High = -50; // 高:物理、碰撞等
|
{
|
||||||
public const int Normal = 0; // 正常:游戏逻辑等
|
}
|
||||||
public const int Low = 50; // 低:音频、特效等
|
|
||||||
public const int Deferred = 100; // 延迟:清理、统计等
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用场景
|
当前生成器会补出:
|
||||||
|
|
||||||
### 系统初始化顺序
|
|
||||||
|
|
||||||
控制系统初始化的顺序,确保依赖关系正确:
|
|
||||||
|
|
||||||
```csharp
|
```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.Abstractions.Systems;
|
||||||
|
using GFramework.Core.Extensions;
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Bases;
|
using GFramework.Core.SourceGenerators.Abstractions.Bases;
|
||||||
using GFramework.Core.Abstractions.Bases;
|
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
// 输入系统最先初始化
|
|
||||||
[Priority(PriorityGroup.Critical)]
|
[Priority(PriorityGroup.Critical)]
|
||||||
public partial class InputSystem : AbstractSystem
|
public partial class InputSystem : AbstractSystem
|
||||||
{
|
{
|
||||||
protected override void OnInit()
|
protected override void OnInit()
|
||||||
{
|
{
|
||||||
// 初始化输入处理
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 物理系统次之
|
[ContextAware]
|
||||||
[Priority(PriorityGroup.High)]
|
public partial class SystemBootstrapper : IController
|
||||||
public partial class PhysicsSystem : AbstractSystem
|
|
||||||
{
|
{
|
||||||
protected override void OnInit()
|
public void Start()
|
||||||
{
|
{
|
||||||
// 初始化物理引擎
|
var systems = this.GetSystemsByPriority<ISystem>();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 游戏逻辑系统在中间
|
|
||||||
[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>();
|
|
||||||
|
|
||||||
foreach (var system in systems)
|
foreach (var system in systems)
|
||||||
{
|
{
|
||||||
system.Init();
|
system.Initialize();
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 事件处理器优先级
|
|
||||||
|
|
||||||
控制事件处理器的执行顺序:
|
|
||||||
|
|
||||||
```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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,496 +106,111 @@ public class EventBus : IEventBus
|
|||||||
|
|
||||||
### 服务排序
|
### 服务排序
|
||||||
|
|
||||||
控制多个服务实现的优先级:
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// 高优先级服务
|
|
||||||
[Priority(PriorityGroup.High)]
|
[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)]
|
[Priority(PriorityGroup.Low)]
|
||||||
public partial class FallbackService : IService
|
public partial class MetricsSaveMigration : ISaveMigration
|
||||||
{
|
{
|
||||||
public void Execute()
|
}
|
||||||
|
|
||||||
|
var migrations = architecture.Context.GetServicesByPriority<ISaveMigration>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## `PriorityGroup` 的角色
|
||||||
|
|
||||||
|
当前仓库提供了这些预定义常量:
|
||||||
|
|
||||||
|
- `PriorityGroup.Critical`
|
||||||
|
- `PriorityGroup.High`
|
||||||
|
- `PriorityGroup.Normal`
|
||||||
|
- `PriorityGroup.Low`
|
||||||
|
- `PriorityGroup.Deferred`
|
||||||
|
|
||||||
|
文档不应该把这些值解释成硬编码的生命周期阶段。它们只是团队共享的排序语义常量,具体“高优先级意味着先做什么”仍然取决于
|
||||||
|
调用方对排序结果的使用方式。
|
||||||
|
|
||||||
|
如果项目有更细粒度的排序约定,也可以直接传 `int`,或在项目层自定义自己的优先级常量。
|
||||||
|
|
||||||
|
## 何时使用 `[Priority]`
|
||||||
|
|
||||||
|
适合以下场景:
|
||||||
|
|
||||||
|
- 类型顺序在编译期就能确定
|
||||||
|
- 你不想手写 `IPrioritized`
|
||||||
|
- 同一类型的所有实例都应共享同一个优先级
|
||||||
|
|
||||||
|
常见例子:
|
||||||
|
|
||||||
|
- 初始化顺序明确的系统
|
||||||
|
- 顺序敏感的服务实现
|
||||||
|
- 有先后要求的处理器或迁移器
|
||||||
|
|
||||||
|
## 何时不要使用 `[Priority]`
|
||||||
|
|
||||||
|
以下场景应改为手写 `IPrioritized`:
|
||||||
|
|
||||||
|
- 优先级要依赖运行时配置
|
||||||
|
- 优先级要根据环境、开关或状态动态变化
|
||||||
|
- 你已经手动实现了 `IPrioritized`
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class DynamicPrioritySystem : IPrioritized
|
||||||
|
{
|
||||||
|
private readonly bool _enabled;
|
||||||
|
|
||||||
|
public DynamicPrioritySystem(bool enabled)
|
||||||
{
|
{
|
||||||
// 最后备选
|
_enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int Priority => _enabled ? PriorityGroup.High : PriorityGroup.Deferred;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 与 PriorityUsageAnalyzer 集成
|
## 当前诊断与约束
|
||||||
|
|
||||||
### GF_Priority_Usage_001 诊断
|
`[Priority]` 当前有几条直接约束:
|
||||||
|
|
||||||
`PriorityUsageAnalyzer` 分析器会检测应该使用 `GetAllByPriority<T>()` 而非 `GetAll<T>()` 的场景:
|
- `GF_Priority_001`
|
||||||
|
- 只能标在 `class`
|
||||||
|
- `GF_Priority_002`
|
||||||
|
- 目标类型已经手写实现 `IPrioritized`
|
||||||
|
- `GF_Priority_003`
|
||||||
|
- 类型必须是 `partial`
|
||||||
|
- `GF_Priority_004`
|
||||||
|
- 特性值缺失或无效
|
||||||
|
- `GF_Priority_005`
|
||||||
|
- 不支持嵌套类
|
||||||
|
|
||||||
**错误示例**:
|
对文档而言,最关键的结论是:
|
||||||
|
|
||||||
```csharp
|
- `partial` 是强约束
|
||||||
// ❌ 不推荐:可能未按优先级排序
|
- 顶层类是强约束
|
||||||
var systems = context.GetAll<ISystem>();
|
- 手写实现与生成实现只能二选一
|
||||||
```
|
|
||||||
|
|
||||||
**正确示例**:
|
## 与旧写法的边界
|
||||||
|
|
||||||
```csharp
|
下面这些旧写法或旧表述已经不再适合作为默认指导:
|
||||||
// ✅ 推荐:确保按优先级排序
|
|
||||||
var systems = context.GetAllByPriority<ISystem>();
|
|
||||||
```
|
|
||||||
|
|
||||||
### 分析器规则
|
- 在 `IContextAware` 类型里统一写 `this.GetAllByPriority<T>()`
|
||||||
|
- 继续用 `system.Init()` 作为系统初始化示例
|
||||||
|
- 把 `[Priority]` 写成“标了就会自动改变执行顺序”
|
||||||
|
|
||||||
当满足以下条件时,分析器会报告 `GF_Priority_Usage_001` 诊断:
|
当前更准确的理解是:
|
||||||
|
|
||||||
1. 类型实现了 `IPrioritized` 接口
|
- `[Priority]` 只生成 `Priority`
|
||||||
2. 使用了 `GetAll<T>()` 方法
|
- 排序效果依赖容器、上下文或扩展方法是否走了 priority-aware API
|
||||||
3. 建议改用 `GetAllByPriority<T>()` 方法
|
- `IContextAware` 路径更推荐按组件类别使用 `GetSystemsByPriority` / `GetServicesByPriority` 等入口
|
||||||
|
|
||||||
## 诊断信息
|
## 推荐阅读
|
||||||
|
|
||||||
### GF_Priority_001 - 只能应用于类
|
1. [context-aware-generator.md](./context-aware-generator.md)
|
||||||
|
2. [context-get-generator.md](./context-get-generator.md)
|
||||||
**错误信息**:`Priority attribute can only be applied to classes`
|
3. [../core/index.md](../core/index.md)
|
||||||
|
4. `GFramework.Core.SourceGenerators/README.md`
|
||||||
**场景**:将 `[Priority]` 特性应用于非类类型
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[Priority(10)]
|
|
||||||
public interface IMyInterface // ❌ 错误
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[Priority(10)]
|
|
||||||
public struct MyStruct // ❌ 错误
|
|
||||||
{
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**解决方案**:只在类上使用 `[Priority]` 特性
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[Priority(10)]
|
|
||||||
public partial class MyClass // ✅ 正确
|
|
||||||
{
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GF_Priority_002 - 已实现 IPrioritized 接口
|
|
||||||
|
|
||||||
**错误信息**:`Type '{ClassName}' already implements IPrioritized interface`
|
|
||||||
|
|
||||||
**场景**:类已手动实现 `IPrioritized` 接口
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[Priority(10)]
|
|
||||||
public partial class MySystem : IPrioritized // ❌ 冲突
|
|
||||||
{
|
|
||||||
public int Priority => 10; // 手动实现
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**解决方案**:移除 `[Priority]` 特性或移除手动实现
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// 方案1:移除特性,使用手动实现
|
|
||||||
public partial class MySystem : IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 方案2:移除手动实现,使用生成器
|
|
||||||
[Priority(10)]
|
|
||||||
public partial class MySystem
|
|
||||||
{
|
|
||||||
// 生成器自动实现
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GF_Priority_003 - 必须声明为 partial
|
|
||||||
|
|
||||||
**错误信息**:`Class '{ClassName}' must be declared as partial`
|
|
||||||
|
|
||||||
**场景**:类未声明为 `partial`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[Priority(10)]
|
|
||||||
public class MySystem // ❌ 缺少 partial
|
|
||||||
{
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**解决方案**:添加 `partial` 关键字
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[Priority(10)]
|
|
||||||
public partial class MySystem // ✅ 正确
|
|
||||||
{
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GF_Priority_004 - 优先级值无效
|
|
||||||
|
|
||||||
**错误信息**:`Priority value is invalid`
|
|
||||||
|
|
||||||
**场景**:特性参数无效或未提供
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[Priority] // ❌ 缺少参数
|
|
||||||
public partial class MySystem
|
|
||||||
{
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**解决方案**:提供有效的优先级值
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[Priority(10)] // ✅ 正确
|
|
||||||
public partial class MySystem
|
|
||||||
{
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GF_Priority_005 - 不支持嵌套类
|
|
||||||
|
|
||||||
**错误信息**:`Nested class '{ClassName}' is not supported`
|
|
||||||
|
|
||||||
**场景**:在嵌套类中使用 `[Priority]` 特性
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public partial class OuterClass
|
|
||||||
{
|
|
||||||
[Priority(10)]
|
|
||||||
public partial class InnerClass // ❌ 错误
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**解决方案**:将嵌套类提取为独立的类
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[Priority(10)]
|
|
||||||
public partial class InnerClass // ✅ 正确
|
|
||||||
{
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 最佳实践
|
|
||||||
|
|
||||||
### 1. 使用 PriorityGroup 常量
|
|
||||||
|
|
||||||
避免使用魔法数字,优先使用预定义常量:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// ✅ 推荐:使用常量
|
|
||||||
[Priority(PriorityGroup.Critical)]
|
|
||||||
public partial class InputSystem { }
|
|
||||||
|
|
||||||
// ❌ 不推荐:魔法数字
|
|
||||||
[Priority(-100)]
|
|
||||||
public partial class InputSystem { }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 预留优先级间隔
|
|
||||||
|
|
||||||
为未来扩展预留间隔:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public static class SystemPriority
|
|
||||||
{
|
|
||||||
public const int Input = -100;
|
|
||||||
public const int PrePhysics = -90; // 预留扩展
|
|
||||||
public const int Physics = -80;
|
|
||||||
public const int PostPhysics = -70; // 预留扩展
|
|
||||||
public const int Gameplay = 0;
|
|
||||||
public const int PostGameplay = 10; // 预留扩展
|
|
||||||
public const int Audio = 50;
|
|
||||||
public const int Cleanup = 100;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 文档化优先级语义
|
|
||||||
|
|
||||||
为自定义优先级值添加注释:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
/// <summary>
|
|
||||||
/// 输入系统,优先级 -100,需要最先初始化以接收输入事件
|
|
||||||
/// </summary>
|
|
||||||
[Priority(PriorityGroup.Critical)]
|
|
||||||
public partial class InputSystem : AbstractSystem
|
|
||||||
{
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 避免优先级冲突
|
|
||||||
|
|
||||||
当多个类有相同优先级时,执行顺序不确定。应避免依赖特定顺序:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// ❌ 不推荐:相同优先级,顺序不确定
|
|
||||||
[Priority(0)]
|
|
||||||
public partial class SystemA { }
|
|
||||||
|
|
||||||
[Priority(0)]
|
|
||||||
public partial class SystemB { }
|
|
||||||
|
|
||||||
// ✅ 推荐:明确区分优先级
|
|
||||||
[Priority(-10)]
|
|
||||||
public partial class SystemA { }
|
|
||||||
|
|
||||||
[Priority(0)]
|
|
||||||
public partial class SystemB { }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 只在真正需要排序的场景使用
|
|
||||||
|
|
||||||
不要滥用优先级特性,只在确实需要排序的场景使用:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// ✅ 推荐:需要排序的系统
|
|
||||||
[Priority(PriorityGroup.Critical)]
|
|
||||||
public partial class InputSystem : AbstractSystem { }
|
|
||||||
|
|
||||||
// ❌ 不推荐:不需要排序的工具类
|
|
||||||
[Priority(10)]
|
|
||||||
public static partial class MathHelper // 静态工具类无需优先级
|
|
||||||
{
|
|
||||||
public static int Add(int a, int b) => a + b;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 高级场景
|
|
||||||
|
|
||||||
### 泛型类支持
|
|
||||||
|
|
||||||
Priority 特性支持泛型类:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[Priority(20)]
|
|
||||||
public partial class GenericSystem<T> : ISystem
|
|
||||||
{
|
|
||||||
public void Init()
|
|
||||||
{
|
|
||||||
// 泛型系统的初始化
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 与其他特性组合
|
|
||||||
|
|
||||||
Priority 可以与其他源代码生成器特性组合使用:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Bases;
|
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Logging;
|
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Rule;
|
|
||||||
|
|
||||||
[Priority(PriorityGroup.High)]
|
|
||||||
[Log]
|
|
||||||
[ContextAware]
|
|
||||||
public partial class HighPrioritySystem : AbstractSystem
|
|
||||||
{
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
Logger.Info("High priority system initialized");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 运行时优先级查询
|
|
||||||
|
|
||||||
可以在运行时查询优先级值:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public void ProcessSystems()
|
|
||||||
{
|
|
||||||
var systems = this.GetAllByPriority<ISystem>();
|
|
||||||
|
|
||||||
foreach (var system in systems)
|
|
||||||
{
|
|
||||||
if (system is IPrioritized prioritized)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"System: {system.GetType().Name}, Priority: {prioritized.Priority}");
|
|
||||||
}
|
|
||||||
|
|
||||||
system.Init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q: 优先级值可以是任意整数吗?
|
|
||||||
|
|
||||||
**A**: 是的,优先级值可以是任何 `int` 类型的值。推荐使用 `PriorityGroup` 预定义常量或在项目中定义自己的优先级常量。
|
|
||||||
|
|
||||||
### Q: 多个类有相同优先级会怎样?
|
|
||||||
|
|
||||||
**A**: 当多个类有相同的优先级值时,它们的相对顺序是不确定的。建议为每个类设置不同的优先级值,或在文档中明确说明相同优先级类的执行顺序不保证。
|
|
||||||
|
|
||||||
### Q: 可以在运行时改变优先级吗?
|
|
||||||
|
|
||||||
**A**: 不可以。`Priority` 属性是只读的,值在编译时确定。如果需要运行时改变优先级,应手动实现 `IPrioritized` 接口。
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class DynamicPrioritySystem : IPrioritized
|
|
||||||
{
|
|
||||||
private int _priority;
|
|
||||||
|
|
||||||
public int Priority => _priority;
|
|
||||||
|
|
||||||
public void SetPriority(int value)
|
|
||||||
{
|
|
||||||
_priority = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q: 优先级支持继承吗?
|
|
||||||
|
|
||||||
**A**: `[Priority]` 特性标记为 `Inherited = false`,不会被子类继承。每个子类需要独立标记优先级。
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[Priority(10)]
|
|
||||||
public partial class BaseSystem { }
|
|
||||||
|
|
||||||
public partial class DerivedSystem : BaseSystem // 不会继承 Priority = 10
|
|
||||||
{
|
|
||||||
// 需要重新标记
|
|
||||||
// [Priority(20)]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q: 如何在不使用特性的情况下实现优先级?
|
|
||||||
|
|
||||||
**A**: 可以手动实现 `IPrioritized` 接口:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class ManualPrioritySystem : IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 10;
|
|
||||||
|
|
||||||
// 手动实现提供更大的灵活性
|
|
||||||
public int Priority => _config.Enabled ? 10 : 100;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Q: 负数优先级和正数优先级的区别是什么?
|
|
||||||
|
|
||||||
**A**: 优先级值用于排序,通常值越小优先级越高:
|
|
||||||
|
|
||||||
- 负数(-100):高优先级,最先执行
|
|
||||||
- 零(0):默认优先级
|
|
||||||
- 正数(100):低优先级,最后执行
|
|
||||||
|
|
||||||
具体含义取决于使用场景,但推荐遵循 `PriorityGroup` 定义的语义。
|
|
||||||
|
|
||||||
## 实际应用示例
|
|
||||||
|
|
||||||
### 游戏系统架构
|
|
||||||
|
|
||||||
完整的游戏系统初始化示例:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
using GFramework.Core.Abstractions.Systems;
|
|
||||||
using GFramework.Core.SourceGenerators.Abstractions.Bases;
|
|
||||||
using GFramework.Core.Abstractions.Bases;
|
|
||||||
|
|
||||||
// 输入系统(最先初始化)
|
|
||||||
[Priority(PriorityGroup.Critical)]
|
|
||||||
[Log]
|
|
||||||
public partial class InputSystem : AbstractSystem
|
|
||||||
{
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
Logger.Info("Input system initialized");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 物理系统
|
|
||||||
[Priority(PriorityGroup.High)]
|
|
||||||
[Log]
|
|
||||||
public partial class PhysicsSystem : AbstractSystem
|
|
||||||
{
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
Logger.Info("Physics system initialized");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 游戏逻辑系统
|
|
||||||
[Priority(PriorityGroup.Normal)]
|
|
||||||
[Log]
|
|
||||||
[ContextAware]
|
|
||||||
public partial class GameplaySystem : AbstractSystem
|
|
||||||
{
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
Logger.Info("Gameplay system initialized");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 音频系统
|
|
||||||
[Priority(PriorityGroup.Low)]
|
|
||||||
[Log]
|
|
||||||
public partial class AudioSystem : AbstractSystem
|
|
||||||
{
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
Logger.Info("Audio system initialized");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理系统(最后执行)
|
|
||||||
[Priority(PriorityGroup.Deferred)]
|
|
||||||
[Log]
|
|
||||||
public partial class CleanupSystem : AbstractSystem
|
|
||||||
{
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
Logger.Info("Cleanup system initialized");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
在架构中初始化:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class GameArchitecture : Architecture
|
|
||||||
{
|
|
||||||
protected override async Task InitAsync()
|
|
||||||
{
|
|
||||||
// 获取所有系统并按优先级排序
|
|
||||||
var systems = this.GetAllByPriority<ISystem>();
|
|
||||||
|
|
||||||
foreach (var system in systems)
|
|
||||||
{
|
|
||||||
await system.InitAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 相关文档
|
|
||||||
|
|
||||||
- [Source Generators 概述](./index.md)
|
|
||||||
- [ContextAware 生成器](./context-aware-generator.md)
|
|
||||||
- [系统初始化](../core/system.md)
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user