mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-13 22:25:37 +08:00
Compare commits
27 Commits
617e0bffd2
...
7cfdd2cf21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cfdd2cf21 | ||
|
|
1753778cae | ||
|
|
953a03b937 | ||
|
|
686647c06b | ||
|
|
99ccc28697 | ||
|
|
4a5e1e74a6 | ||
|
|
a9904a35be | ||
|
|
86cfaa7122 | ||
|
|
b6a9fefda9 | ||
|
|
067d72fada | ||
|
|
1c87272f6b | ||
|
|
1f560635a8 | ||
|
|
5778782df0 | ||
|
|
979db3b5a5 | ||
|
|
72ebd266d3 | ||
|
|
e19e60ea1a | ||
|
|
650618b5ab | ||
|
|
946cdbb9d2 | ||
|
|
9ce634ed1c | ||
|
|
9deafac234 | ||
|
|
c106e53a74 | ||
|
|
fb0a55f435 | ||
|
|
5befaf707b | ||
|
|
8f2d95910e | ||
|
|
1454c81a5b | ||
|
|
e3eec5452c | ||
|
|
7e13752bb1 |
@ -62,6 +62,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
|
||||||
|
- Detailed failed-test rows from GitHub Test Reporter / CTRF comments when the PR comment includes `Name` / `Failure Message` content
|
||||||
- CLI support for writing full JSON to a file and printing only narrowed text sections to stdout
|
- 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
|
||||||
|
|
||||||
|
|||||||
@ -257,6 +257,11 @@ def strip_markdown_links(text: str) -> str:
|
|||||||
return re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
return re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_markdown_images(text: str) -> str:
|
||||||
|
"""Drop Markdown image syntax while keeping surrounding text readable."""
|
||||||
|
return re.sub(r"!\[[^\]]*\]\([^)]+\)", "", text)
|
||||||
|
|
||||||
|
|
||||||
def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str | None:
|
def extract_section(text: str, start_marker: str, end_markers: list[str]) -> str | None:
|
||||||
"""Extract text between a start marker and the earliest matching end marker."""
|
"""Extract text between a start marker and the earliest matching end marker."""
|
||||||
start = text.find(start_marker)
|
start = text.find(start_marker)
|
||||||
@ -486,43 +491,198 @@ def parse_megalinter_comment(comment_body: str) -> dict[str, Any]:
|
|||||||
return report
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def clean_markdown_table_cell(text: str) -> str:
|
||||||
|
"""Normalize a Markdown table cell for structured parsing."""
|
||||||
|
cleaned = strip_markdown_images(strip_markdown_links(html.unescape(text)))
|
||||||
|
cleaned = cleaned.replace("\xa0", " ")
|
||||||
|
cleaned = cleaned.replace("**", "").replace("*", "").replace("`", "")
|
||||||
|
return collapse_whitespace(cleaned)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_int_from_text(text: str) -> int | None:
|
||||||
|
"""Extract the first integer value from text."""
|
||||||
|
match = re.search(r"\d+", text)
|
||||||
|
return int(match.group(0)) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_duration_from_text(text: str) -> str:
|
||||||
|
"""Extract a duration token from text when present."""
|
||||||
|
match = re.search(r"\d+(?:\.\d+)?(?:ms|s|m|h)", text)
|
||||||
|
if match is not None:
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
return collapse_whitespace(text)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_markdown_table(table_text: str) -> tuple[list[str], list[list[str]]]:
|
||||||
|
"""Parse a Markdown table into header cells and row cells."""
|
||||||
|
lines = [line.strip() for line in table_text.splitlines() if line.strip().startswith("|")]
|
||||||
|
if len(lines) < 2:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
headers = [clean_markdown_table_cell(cell) for cell in lines[0].strip("|").split("|")]
|
||||||
|
rows: list[list[str]] = []
|
||||||
|
for line in lines[2:]:
|
||||||
|
cells = [clean_markdown_table_cell(cell) for cell in line.strip("|").split("|")]
|
||||||
|
if cells:
|
||||||
|
rows.append(cells)
|
||||||
|
|
||||||
|
return headers, rows
|
||||||
|
|
||||||
|
|
||||||
|
def extract_markdown_table_after_heading(block: str, heading: str) -> tuple[list[str], list[list[str]]]:
|
||||||
|
"""Extract the first Markdown table that appears after a heading."""
|
||||||
|
section = extract_section(block, heading, ["\n### ", "\n#### ", "\n<details>", "\n<table>", "\n<sub>"])
|
||||||
|
if section is None:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
table_match = re.search(r"(\|.*\|\n\|[-| :]+\|\n(?:\|.*\|\n?)*)", section, re.S)
|
||||||
|
if table_match is None:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
return parse_markdown_table(table_match.group(1))
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_stat_header(header: str) -> str:
|
||||||
|
"""Normalize a human-readable stats header into a stable machine key."""
|
||||||
|
ascii_only = re.sub(r"[^A-Za-z]+", "", header).lower()
|
||||||
|
aliases = {
|
||||||
|
"tests": "tests",
|
||||||
|
"passed": "passed",
|
||||||
|
"failed": "failed",
|
||||||
|
"skipped": "skipped",
|
||||||
|
"pending": "pending",
|
||||||
|
"other": "other",
|
||||||
|
"flaky": "flaky",
|
||||||
|
"duration": "duration",
|
||||||
|
}
|
||||||
|
return aliases.get(ascii_only, ascii_only)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_stats_table(headers: list[str], rows: list[list[str]]) -> dict[str, Any]:
|
||||||
|
"""Convert a parsed Markdown stats table into the report stats shape."""
|
||||||
|
if not headers or not rows:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
first_row = rows[0]
|
||||||
|
stats: dict[str, Any] = {}
|
||||||
|
for header, value in zip(headers, first_row):
|
||||||
|
key = normalize_stat_header(header)
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if key == "duration":
|
||||||
|
stats[key] = parse_duration_from_text(value)
|
||||||
|
continue
|
||||||
|
|
||||||
|
parsed_value = parse_int_from_text(value)
|
||||||
|
if parsed_value is not None:
|
||||||
|
stats[key] = parsed_value
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_failure_message(text: str) -> str:
|
||||||
|
"""Normalize a failed-test message while preserving the meaningful lines."""
|
||||||
|
cleaned = html.unescape(text)
|
||||||
|
cleaned = re.sub(r"(?i)<br\s*/?>", "\n", cleaned)
|
||||||
|
cleaned = re.sub(r"</?(?:p|div|tbody|thead|tr|td|th|table)>", "\n", cleaned)
|
||||||
|
cleaned = re.sub(r"<[^>]+>", " ", cleaned)
|
||||||
|
lines = [collapse_whitespace(line) for line in cleaned.splitlines()]
|
||||||
|
meaningful_lines = [line for line in lines if line]
|
||||||
|
return "\n".join(meaningful_lines)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_failed_test_summary_list(block: str) -> list[str]:
|
||||||
|
"""Parse the compact failed-tests summary list from CTRF details blocks."""
|
||||||
|
failed_tests_section = re.search(
|
||||||
|
r"<details><summary><strong>\s*Failed Tests.*?</summary>(?P<body>.*?)</details>",
|
||||||
|
block,
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
if failed_tests_section is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
summary_body = strip_markdown_links(strip_markdown_images(html.unescape(failed_tests_section.group("body"))))
|
||||||
|
failed_tests: list[str] = []
|
||||||
|
for raw_line in summary_body.splitlines():
|
||||||
|
line = collapse_whitespace(raw_line)
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "arrow-right" in raw_line:
|
||||||
|
parts = [part.strip() for part in line.split("arrow-right") if part.strip()]
|
||||||
|
candidate = parts[-1] if parts else line
|
||||||
|
elif ">" in line:
|
||||||
|
candidate = line.split(">")[-1].strip()
|
||||||
|
else:
|
||||||
|
candidate = line
|
||||||
|
|
||||||
|
if candidate:
|
||||||
|
failed_tests.append(candidate)
|
||||||
|
|
||||||
|
return failed_tests
|
||||||
|
|
||||||
|
|
||||||
|
def parse_failed_test_details(block: str) -> list[dict[str, str]]:
|
||||||
|
"""Parse the detailed failed-test HTML table from GitHub Test Reporter comments."""
|
||||||
|
details: list[dict[str, str]] = []
|
||||||
|
table_section = re.search(
|
||||||
|
r"### ❌ \*\*Some tests failed!\*\*.*?<tbody>(?P<body>.*?)</tbody>",
|
||||||
|
block,
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
if table_section is None:
|
||||||
|
return details
|
||||||
|
|
||||||
|
row_pattern = re.compile(
|
||||||
|
r"<tr>\s*<td>(?P<name>.*?)</td>\s*<td>(?P<message>.*?)</td>(?:\s*<td>.*?</td>)*\s*</tr>",
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test Reporter tables may grow extra columns over time; only the first two are required here.
|
||||||
|
for row_match in row_pattern.finditer(table_section.group("body")):
|
||||||
|
name_cell = row_match.group("name")
|
||||||
|
message_cell = row_match.group("message")
|
||||||
|
name = collapse_whitespace(strip_tags(html.unescape(name_cell))).lstrip("❌").strip()
|
||||||
|
failure_message = normalize_failure_message(message_cell)
|
||||||
|
if name:
|
||||||
|
details.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"failure_message": failure_message,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
def parse_test_report(block: str) -> dict[str, Any]:
|
def parse_test_report(block: str) -> dict[str, Any]:
|
||||||
"""Parse a CTRF or GitHub test-reporter comment block."""
|
"""Parse a CTRF or GitHub test-reporter comment block."""
|
||||||
report: dict[str, Any] = {
|
report: dict[str, Any] = {
|
||||||
"raw": block.strip(),
|
"raw": block.strip(),
|
||||||
"stats": {},
|
"stats": {},
|
||||||
"failed_tests": [],
|
"failed_tests": [],
|
||||||
|
"failed_test_details": [],
|
||||||
"has_failed_tests": False,
|
"has_failed_tests": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
summary_row_match = re.search(
|
summary_headers, summary_rows = extract_markdown_table_after_heading(block, "### Summary")
|
||||||
r"\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|"
|
report["stats"] = parse_stats_table(summary_headers, summary_rows)
|
||||||
r"\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?(\d+)\*?\*?\s*\|\s*\*?\*?([^\|]+?)\*?\*?\s*\|",
|
|
||||||
block,
|
|
||||||
)
|
|
||||||
if summary_row_match is not None:
|
|
||||||
report["stats"] = {
|
|
||||||
"tests": int(summary_row_match.group(1)),
|
|
||||||
"passed": int(summary_row_match.group(2)),
|
|
||||||
"failed": int(summary_row_match.group(3)),
|
|
||||||
"skipped": int(summary_row_match.group(4)),
|
|
||||||
"other": int(summary_row_match.group(5)),
|
|
||||||
"flaky": int(summary_row_match.group(6)),
|
|
||||||
"duration": summary_row_match.group(7).strip(),
|
|
||||||
}
|
|
||||||
|
|
||||||
failed_tests_section = extract_section(
|
if not report["stats"]:
|
||||||
block,
|
build_headers, build_rows = extract_markdown_table_after_heading(block, "### build-and-test:")
|
||||||
"### Failed Tests",
|
report["stats"] = parse_stats_table(build_headers, build_rows)
|
||||||
["### Slowest Tests", "### Insights", "<sub>", "[Github Test Reporter]"],
|
|
||||||
)
|
failed_test_details = parse_failed_test_details(block)
|
||||||
if failed_tests_section:
|
failed_test_names = parse_failed_test_summary_list(block)
|
||||||
lines = [line.strip("- ").strip() for line in failed_tests_section.splitlines()[1:] if line.strip()]
|
if not failed_test_names and failed_test_details:
|
||||||
report["failed_tests"] = lines
|
failed_test_names = [detail["name"] for detail in failed_test_details]
|
||||||
report["has_failed_tests"] = True
|
|
||||||
elif "No failed tests in this run." in block or "All tests passed!" in block:
|
report["failed_tests"] = failed_test_names
|
||||||
report["failed_tests"] = []
|
report["failed_test_details"] = failed_test_details
|
||||||
report["has_failed_tests"] = False
|
failed_count = int(report["stats"].get("failed", 0) or 0)
|
||||||
|
report["has_failed_tests"] = bool(failed_test_names or failed_test_details or failed_count > 0)
|
||||||
|
|
||||||
return report
|
return report
|
||||||
|
|
||||||
@ -1103,8 +1263,17 @@ def format_text(
|
|||||||
lines.append(f"- Report {index}: no structured test stats parsed")
|
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"]:
|
failed_test_details = report.get("failed_test_details", [])
|
||||||
lines.append(f" Failed test: {truncate_text(failed_test, max_description_length)}")
|
if failed_test_details:
|
||||||
|
for failed_test_detail in failed_test_details:
|
||||||
|
lines.append(f" Failed test: {truncate_text(failed_test_detail['name'], max_description_length)}")
|
||||||
|
lines.append(
|
||||||
|
" Failure: "
|
||||||
|
f"{truncate_text(failed_test_detail['failure_message'].replace(chr(10), ' | '), max_description_length)}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for failed_test in report["failed_tests"]:
|
||||||
|
lines.append(f" Failed test: {truncate_text(failed_test, max_description_length)}")
|
||||||
else:
|
else:
|
||||||
lines.append(" Failed tests: none reported")
|
lines.append(" Failed tests: none reported")
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Regression tests for the GFramework PR review fetch helper."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT_PATH = Path(__file__).with_name("fetch_current_pr_review.py")
|
||||||
|
MODULE_SPEC = importlib.util.spec_from_file_location("fetch_current_pr_review", SCRIPT_PATH)
|
||||||
|
if MODULE_SPEC is None or MODULE_SPEC.loader is None:
|
||||||
|
raise RuntimeError(f"Unable to load module from {SCRIPT_PATH}.")
|
||||||
|
|
||||||
|
MODULE = importlib.util.module_from_spec(MODULE_SPEC)
|
||||||
|
MODULE_SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
class ParseFailedTestDetailsTests(unittest.TestCase):
|
||||||
|
"""Cover failed-test table parsing edge cases for CTRF comments."""
|
||||||
|
|
||||||
|
def test_parse_failed_test_details_ignores_trailing_columns(self) -> None:
|
||||||
|
"""Extra columns should not prevent extracting the name and failure message."""
|
||||||
|
block = """
|
||||||
|
### ❌ **Some tests failed!**
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>❌ RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache</td>
|
||||||
|
<td><pre>Expected: False\nBut was: True</pre></td>
|
||||||
|
<td>failed</td>
|
||||||
|
<td>35.3s</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
details = MODULE.parse_failed_test_details(block)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
details,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "RegisterMigration_During_Cache_Rebuild_Should_Not_Leave_Stale_Type_Cache",
|
||||||
|
"failure_message": "Expected: False\nBut was: True",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -36,18 +36,17 @@
|
|||||||
|
|
||||||
## XML 阅读入口
|
## XML 阅读入口
|
||||||
|
|
||||||
截至 `2026-04-22`,下面这份目录视图可以帮助你快速定位 `GFramework.Core.Abstractions` 的类型级 XML 文档入口;当前契约目录族的类型声明都已带
|
下面这份目录视图可以帮助你快速定位 `GFramework.Core.Abstractions` 的代表类型。更细的契约约束与交互语义,适合在阅读具体接口和成员时继续结合源码确认。
|
||||||
XML 注释。更细的契约约束与交互语义,适合在阅读具体接口和成员时继续结合源码确认。
|
|
||||||
|
|
||||||
| 类型族 | 基线状态 | 代表类型 |
|
| 类型族 | 代表类型 | 阅读重点 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `Architectures/` `Lifecycle/` `Registries/` | `20/20` 个类型声明已带 XML 注释 | `IArchitecture`、`IArchitectureContext`、`IServiceModule`、`KeyValueRegistryBase<TKey, TValue>` |
|
| `Architectures/` `Lifecycle/` `Registries/` | `IArchitecture`、`IArchitectureContext`、`IServiceModule`、`KeyValueRegistryBase<TKey, TValue>` | 看架构、上下文、模块装配与注册表基类边界 |
|
||||||
| `Command/` `Query/` `Cqrs/` | `10/10` 个类型声明已带 XML 注释 | `ICommandExecutor`、`IAsyncQueryExecutor`、`ICqrsRuntime` |
|
| `Command/` `Query/` `Cqrs/` | `ICommandExecutor`、`IAsyncQueryExecutor`、`ICqrsRuntime` | 看命令、查询与新请求模型的调用入口 |
|
||||||
| `Events/` `Property/` `State/` `StateManagement/` | `25/25` 个类型声明已带 XML 注释 | `IEventBus`、`IBindableProperty<T>`、`IStateMachine`、`IStore<TState>` |
|
| `Events/` `Property/` `State/` `StateManagement/` | `IEventBus`、`IBindableProperty<T>`、`IStateMachine`、`IStore<TState>` | 看事件分发、可绑定状态与 store 契约 |
|
||||||
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `17/17` 个类型声明已带 XML 注释 | `IYieldInstruction`、`ITimeProvider`、`IPauseStackManager`、`IAsyncKeyLockManager` |
|
| `Coroutine/` `Time/` `Pause/` `Concurrency/` | `IYieldInstruction`、`ITimeProvider`、`IPauseStackManager`、`IAsyncKeyLockManager` | 看协程、时间源、暂停栈与并发协调能力 |
|
||||||
| `Resource/` `Pool/` `Logging/` `Localization/` | `27/27` 个类型声明已带 XML 注释 | `IResourceManager`、`IObjectPoolSystem`、`ILogger`、`ILocalizationManager` |
|
| `Resource/` `Pool/` `Logging/` `Localization/` | `IResourceManager`、`IObjectPoolSystem`、`ILogger`、`ILocalizationManager` | 看资源、对象池、日志与本地化服务角色 |
|
||||||
| `Configuration/` `Environment/` `Data/` `Serializer/` `Storage/` `Versioning/` | `7/7` 个类型声明已带 XML 注释 | `IConfigurationManager`、`IEnvironment`、`ILoadableFrom<T>`、`ISerializer`、`IStorage` |
|
| `Configuration/` `Environment/` `Data/` `Serializer/` `Storage/` `Versioning/` | `IConfigurationManager`、`IEnvironment`、`ILoadableFrom<T>`、`ISerializer`、`IStorage` | 看配置、环境、数据装载、序列化与存储边界 |
|
||||||
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` `Enums/` `Properties/` | `19/19` 个类型声明已带 XML 注释 | `IPrioritized`、`IController`、`IModel`、`ISystem`、`IContextUtility`、`ArchitecturePhase` |
|
| `Bases/` `Controller/` `Model/` `Systems/` `Utility/` `Rule/` `Enums/` `Properties/` | `IPrioritized`、`IController`、`IModel`、`ISystem`、`IContextUtility`、`ArchitecturePhase` | 看组件角色、优先级和值对象约定 |
|
||||||
|
|
||||||
完整接入说明与阅读顺序见 [Core 抽象层说明](../docs/zh-CN/abstractions/core-abstractions.md)。
|
完整接入说明与阅读顺序见 [Core 抽象层说明](../docs/zh-CN/abstractions/core-abstractions.md)。
|
||||||
|
|
||||||
|
|||||||
@ -176,6 +176,6 @@ public sealed class TrackingPipelineBehavior<TRequest, TResponse> : IPipelineBeh
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
InvocationCount++;
|
InvocationCount++;
|
||||||
return await next(message, cancellationToken);
|
return await next(message, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
GFramework.Core.Tests/Architectures/IMixedTestSystem.cs
Normal file
10
GFramework.Core.Tests/Architectures/IMixedTestSystem.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using GFramework.Core.Abstractions.Systems;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义用于混合优先级排序测试的系统契约。
|
||||||
|
/// </summary>
|
||||||
|
public interface IMixedTestSystem : ISystem
|
||||||
|
{
|
||||||
|
}
|
||||||
10
GFramework.Core.Tests/Architectures/IPriorityTestModel.cs
Normal file
10
GFramework.Core.Tests/Architectures/IPriorityTestModel.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using GFramework.Core.Abstractions.Model;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义用于优先级排序测试的模型契约。
|
||||||
|
/// </summary>
|
||||||
|
public interface IPriorityTestModel : IModel
|
||||||
|
{
|
||||||
|
}
|
||||||
10
GFramework.Core.Tests/Architectures/IPriorityTestSystem.cs
Normal file
10
GFramework.Core.Tests/Architectures/IPriorityTestSystem.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using GFramework.Core.Abstractions.Systems;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义用于优先级排序测试的系统契约。
|
||||||
|
/// </summary>
|
||||||
|
public interface IPriorityTestSystem : ISystem
|
||||||
|
{
|
||||||
|
}
|
||||||
10
GFramework.Core.Tests/Architectures/IPriorityTestUtility.cs
Normal file
10
GFramework.Core.Tests/Architectures/IPriorityTestUtility.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using GFramework.Core.Abstractions.Utility;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定义用于优先级排序测试的工具契约。
|
||||||
|
/// </summary>
|
||||||
|
public interface IPriorityTestUtility : IUtility
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示显式声明负优先级的混合测试系统。
|
||||||
|
/// </summary>
|
||||||
|
public class MixedTestSystemNegativePriority : AbstractSystem, IMixedTestSystem, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试系统的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => -10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示显式声明正优先级的混合测试系统。
|
||||||
|
/// </summary>
|
||||||
|
public class MixedTestSystemWithPriority : AbstractSystem, IMixedTestSystem, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试系统的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示未声明优先级、依赖默认排序值的混合测试系统。
|
||||||
|
/// </summary>
|
||||||
|
public class MixedTestSystemWithoutPriority : AbstractSystem, IMixedTestSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,7 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using GFramework.Core.Abstractions.Bases;
|
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Abstractions.Model;
|
|
||||||
using GFramework.Core.Abstractions.Systems;
|
|
||||||
using GFramework.Core.Abstractions.Utility;
|
|
||||||
using GFramework.Core.Ioc;
|
using GFramework.Core.Ioc;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
using GFramework.Core.Model;
|
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Architectures;
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
@ -120,129 +115,3 @@ public class PriorityServiceTests
|
|||||||
Assert.That(systems[2], Is.InstanceOf<MixedTestSystemWithPriority>()); // 10
|
Assert.That(systems[2], Is.InstanceOf<MixedTestSystemWithPriority>()); // 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Test Interfaces
|
|
||||||
|
|
||||||
public interface IPriorityTestSystem : ISystem
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IPriorityTestModel : IModel
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IPriorityTestUtility : IUtility
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IMixedTestSystem : ISystem
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Test Systems
|
|
||||||
|
|
||||||
public class PriorityTestSystemA : AbstractSystem, IPriorityTestSystem, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 10;
|
|
||||||
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PriorityTestSystemB : AbstractSystem, IPriorityTestSystem, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 20;
|
|
||||||
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PriorityTestSystemC : AbstractSystem, IPriorityTestSystem, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 30;
|
|
||||||
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MixedTestSystemWithPriority : AbstractSystem, IMixedTestSystem, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 10;
|
|
||||||
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MixedTestSystemWithoutPriority : AbstractSystem, IMixedTestSystem
|
|
||||||
{
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MixedTestSystemNegativePriority : AbstractSystem, IMixedTestSystem, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => -10;
|
|
||||||
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Test Models
|
|
||||||
|
|
||||||
public class PriorityTestModelA : AbstractModel, IPriorityTestModel, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 10;
|
|
||||||
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PriorityTestModelB : AbstractModel, IPriorityTestModel, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 20;
|
|
||||||
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PriorityTestModelC : AbstractModel, IPriorityTestModel, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 30;
|
|
||||||
|
|
||||||
protected override void OnInit()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Test Utilities
|
|
||||||
|
|
||||||
public class PriorityTestUtilityA : IPriorityTestUtility, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PriorityTestUtilityB : IPriorityTestUtility, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PriorityTestUtilityC : IPriorityTestUtility, IPrioritized
|
|
||||||
{
|
|
||||||
public int Priority => 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|||||||
22
GFramework.Core.Tests/Architectures/PriorityTestModelA.cs
Normal file
22
GFramework.Core.Tests/Architectures/PriorityTestModelA.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
using GFramework.Core.Model;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示优先级为 10 的测试模型。
|
||||||
|
/// </summary>
|
||||||
|
public class PriorityTestModelA : AbstractModel, IPriorityTestModel, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试模型的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
22
GFramework.Core.Tests/Architectures/PriorityTestModelB.cs
Normal file
22
GFramework.Core.Tests/Architectures/PriorityTestModelB.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
using GFramework.Core.Model;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示优先级为 20 的测试模型。
|
||||||
|
/// </summary>
|
||||||
|
public class PriorityTestModelB : AbstractModel, IPriorityTestModel, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试模型的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
22
GFramework.Core.Tests/Architectures/PriorityTestModelC.cs
Normal file
22
GFramework.Core.Tests/Architectures/PriorityTestModelC.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
using GFramework.Core.Model;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示优先级为 30 的测试模型。
|
||||||
|
/// </summary>
|
||||||
|
public class PriorityTestModelC : AbstractModel, IPriorityTestModel, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试模型的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
21
GFramework.Core.Tests/Architectures/PriorityTestSystemA.cs
Normal file
21
GFramework.Core.Tests/Architectures/PriorityTestSystemA.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示优先级为 10 的测试系统。
|
||||||
|
/// </summary>
|
||||||
|
public class PriorityTestSystemA : AbstractSystem, IPriorityTestSystem, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试系统的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
21
GFramework.Core.Tests/Architectures/PriorityTestSystemB.cs
Normal file
21
GFramework.Core.Tests/Architectures/PriorityTestSystemB.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示优先级为 20 的测试系统。
|
||||||
|
/// </summary>
|
||||||
|
public class PriorityTestSystemB : AbstractSystem, IPriorityTestSystem, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试系统的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
21
GFramework.Core.Tests/Architectures/PriorityTestSystemC.cs
Normal file
21
GFramework.Core.Tests/Architectures/PriorityTestSystemC.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示优先级为 30 的测试系统。
|
||||||
|
/// </summary>
|
||||||
|
public class PriorityTestSystemC : AbstractSystem, IPriorityTestSystem, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试系统的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保持空初始化,以便测试仅覆盖优先级排序行为。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
14
GFramework.Core.Tests/Architectures/PriorityTestUtilityA.cs
Normal file
14
GFramework.Core.Tests/Architectures/PriorityTestUtilityA.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示优先级为 10 的测试工具。
|
||||||
|
/// </summary>
|
||||||
|
public class PriorityTestUtilityA : IPriorityTestUtility, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试工具的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 10;
|
||||||
|
}
|
||||||
14
GFramework.Core.Tests/Architectures/PriorityTestUtilityB.cs
Normal file
14
GFramework.Core.Tests/Architectures/PriorityTestUtilityB.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示优先级为 20 的测试工具。
|
||||||
|
/// </summary>
|
||||||
|
public class PriorityTestUtilityB : IPriorityTestUtility, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试工具的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 20;
|
||||||
|
}
|
||||||
14
GFramework.Core.Tests/Architectures/PriorityTestUtilityC.cs
Normal file
14
GFramework.Core.Tests/Architectures/PriorityTestUtilityC.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示优先级为 30 的测试工具。
|
||||||
|
/// </summary>
|
||||||
|
public class PriorityTestUtilityC : IPriorityTestUtility, IPrioritized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前测试工具的排序优先级。
|
||||||
|
/// </summary>
|
||||||
|
public int Priority => 30;
|
||||||
|
}
|
||||||
@ -47,9 +47,11 @@ public sealed class AsyncKeyLockManagerTests
|
|||||||
var index = i;
|
var index = i;
|
||||||
tasks.Add(Task.Run(async () =>
|
tasks.Add(Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var handle = await manager.AcquireLockAsync("same-key").ConfigureAwait(false);
|
await using ((await manager.AcquireLockAsync("same-key").ConfigureAwait(false)).ConfigureAwait(false))
|
||||||
executionOrder.Add(index);
|
{
|
||||||
await Task.Delay(10).ConfigureAwait(false);
|
executionOrder.Add(index);
|
||||||
|
await Task.Delay(10).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,11 +77,13 @@ public sealed class AsyncKeyLockManagerTests
|
|||||||
var key = $"key-{i}";
|
var key = $"key-{i}";
|
||||||
tasks.Add(Task.Run(async () =>
|
tasks.Add(Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var handle = await manager.AcquireLockAsync(key).ConfigureAwait(false);
|
await using ((await manager.AcquireLockAsync(key).ConfigureAwait(false)).ConfigureAwait(false))
|
||||||
var current = Interlocked.Increment(ref concurrentCount);
|
{
|
||||||
maxConcurrent = Math.Max(maxConcurrent, current);
|
var current = Interlocked.Increment(ref concurrentCount);
|
||||||
await Task.Delay(50).ConfigureAwait(false);
|
maxConcurrent = Math.Max(maxConcurrent, current);
|
||||||
Interlocked.Decrement(ref concurrentCount);
|
await Task.Delay(50).ConfigureAwait(false);
|
||||||
|
Interlocked.Decrement(ref concurrentCount);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,8 +121,10 @@ public sealed class AsyncKeyLockManagerTests
|
|||||||
var key = $"key-{i % 10}";
|
var key = $"key-{i % 10}";
|
||||||
tasks.Add(Task.Run(async () =>
|
tasks.Add(Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var handle = await manager.AcquireLockAsync(key).ConfigureAwait(false);
|
await using ((await manager.AcquireLockAsync(key).ConfigureAwait(false)).ConfigureAwait(false))
|
||||||
await Task.Delay(1).ConfigureAwait(false);
|
{
|
||||||
|
await Task.Delay(1).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,10 +145,12 @@ public sealed class AsyncKeyLockManagerTests
|
|||||||
{
|
{
|
||||||
tasks.Add(Task.Run(async () =>
|
tasks.Add(Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var handle = await manager.AcquireLockAsync("same-key").ConfigureAwait(false);
|
await using ((await manager.AcquireLockAsync("same-key").ConfigureAwait(false)).ConfigureAwait(false))
|
||||||
var temp = counter;
|
{
|
||||||
await Task.Delay(1).ConfigureAwait(false);
|
var temp = counter;
|
||||||
counter = temp + 1;
|
await Task.Delay(1).ConfigureAwait(false);
|
||||||
|
counter = temp + 1;
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,8 +303,10 @@ public sealed class AsyncKeyLockManagerTests
|
|||||||
{
|
{
|
||||||
for (var j = 0; j < 10; j++)
|
for (var j = 0; j < 10; j++)
|
||||||
{
|
{
|
||||||
await using var handle = await manager.AcquireLockAsync($"key-{j % 5}").ConfigureAwait(false);
|
await using ((await manager.AcquireLockAsync($"key-{j % 5}").ConfigureAwait(false)).ConfigureAwait(false))
|
||||||
await Task.Delay(10).ConfigureAwait(false);
|
{
|
||||||
|
await Task.Delay(10).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -225,23 +225,31 @@ public class AsyncExtensionsTests
|
|||||||
/// 测试WithRetry方法遵守ShouldRetry谓词
|
/// 测试WithRetry方法遵守ShouldRetry谓词
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task WithRetry_Should_Respect_ShouldRetry_Predicate()
|
public void WithRetry_Should_Respect_ShouldRetry_Predicate()
|
||||||
{
|
{
|
||||||
|
static Task<int> ThrowShouldNotRetry(string parameterName)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Should not retry", parameterName);
|
||||||
|
}
|
||||||
|
|
||||||
// Arrange
|
// Arrange
|
||||||
var attemptCount = 0;
|
var attemptCount = 0;
|
||||||
Func<Task<int>> taskFactory = () =>
|
Func<Task<int>> taskFactory = () =>
|
||||||
{
|
{
|
||||||
attemptCount++;
|
attemptCount++;
|
||||||
throw new ArgumentException("Should not retry");
|
return ThrowShouldNotRetry(nameof(taskFactory));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
Assert.ThrowsAsync<AggregateException>(() =>
|
var exception = Assert.ThrowsAsync<AggregateException>(() =>
|
||||||
taskFactory.WithRetryAsync(3, TimeSpan.FromMilliseconds(10),
|
taskFactory.WithRetryAsync(3, TimeSpan.FromMilliseconds(10),
|
||||||
ex => ex is not ArgumentException));
|
ex => ex is not ArgumentException));
|
||||||
|
|
||||||
await Task.Delay(50).ConfigureAwait(false); // 等待任务完成
|
|
||||||
Assert.That(attemptCount, Is.EqualTo(1)); // 不应该重试
|
Assert.That(attemptCount, Is.EqualTo(1)); // 不应该重试
|
||||||
|
Assert.That(exception, Is.Not.Null);
|
||||||
|
Assert.That(exception!.InnerExceptions, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(exception.InnerExceptions[0], Is.TypeOf<ArgumentException>());
|
||||||
|
Assert.That(((ArgumentException)exception.InnerExceptions[0]).ParamName, Is.EqualTo(nameof(taskFactory)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
8
GFramework.Core.Tests/Ioc/AliasAwareService.cs
Normal file
8
GFramework.Core.Tests/Ioc/AliasAwareService.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace GFramework.Core.Tests.Ioc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 同时实现多个别名接口的测试服务。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService
|
||||||
|
{
|
||||||
|
}
|
||||||
12
GFramework.Core.Tests/Ioc/IMixedService.cs
Normal file
12
GFramework.Core.Tests/Ioc/IMixedService.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace GFramework.Core.Tests.Ioc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 混合服务接口(用于测试优先级和非优先级混合)
|
||||||
|
/// </summary>
|
||||||
|
public interface IMixedService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置服务名称。
|
||||||
|
/// </summary>
|
||||||
|
string? Name { get; set; }
|
||||||
|
}
|
||||||
6
GFramework.Core.Tests/Ioc/IPrimaryAliasService.cs
Normal file
6
GFramework.Core.Tests/Ioc/IPrimaryAliasService.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace GFramework.Core.Tests.Ioc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 主服务别名接口。
|
||||||
|
/// </summary>
|
||||||
|
public interface IPrimaryAliasService : ISharedAliasService;
|
||||||
10
GFramework.Core.Tests/Ioc/IPrioritizedService.cs
Normal file
10
GFramework.Core.Tests/Ioc/IPrioritizedService.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using GFramework.Core.Abstractions.Bases;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Ioc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 优先级服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IPrioritizedService : IPrioritized, IMixedService
|
||||||
|
{
|
||||||
|
}
|
||||||
6
GFramework.Core.Tests/Ioc/ISecondaryAliasService.cs
Normal file
6
GFramework.Core.Tests/Ioc/ISecondaryAliasService.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace GFramework.Core.Tests.Ioc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次级兼容别名接口。
|
||||||
|
/// </summary>
|
||||||
|
public interface ISecondaryAliasService : ISharedAliasService;
|
||||||
6
GFramework.Core.Tests/Ioc/IService.cs
Normal file
6
GFramework.Core.Tests/Ioc/IService.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace GFramework.Core.Tests.Ioc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务接口定义
|
||||||
|
/// </summary>
|
||||||
|
public interface IService;
|
||||||
6
GFramework.Core.Tests/Ioc/ISharedAliasService.cs
Normal file
6
GFramework.Core.Tests/Ioc/ISharedAliasService.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace GFramework.Core.Tests.Ioc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于验证未冻结查询路径中的服务别名去重行为。
|
||||||
|
/// </summary>
|
||||||
|
public interface ISharedAliasService;
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using GFramework.Core.Abstractions.Bases;
|
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Ioc;
|
using GFramework.Core.Ioc;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
@ -734,74 +733,3 @@ public class MicrosoftDiContainerTests
|
|||||||
Assert.That(((IPrioritizedService)services[1]).Priority, Is.EqualTo(30));
|
Assert.That(((IPrioritizedService)services[1]).Priority, Is.EqualTo(30));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 服务接口定义
|
|
||||||
/// </summary>
|
|
||||||
public interface IService;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试服务类,实现 IService 接口
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TestService : IService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置优先级
|
|
||||||
/// </summary>
|
|
||||||
public int Priority { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 优先级服务接口
|
|
||||||
/// </summary>
|
|
||||||
public interface IPrioritizedService : IPrioritized
|
|
||||||
{
|
|
||||||
string? Name { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 混合服务接口(用于测试优先级和非优先级混合)
|
|
||||||
/// </summary>
|
|
||||||
public interface IMixedService
|
|
||||||
{
|
|
||||||
string? Name { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用于验证未冻结查询路径中的服务别名去重行为。
|
|
||||||
/// </summary>
|
|
||||||
public interface ISharedAliasService;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 主服务别名接口。
|
|
||||||
/// </summary>
|
|
||||||
public interface IPrimaryAliasService : ISharedAliasService;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 次级兼容别名接口。
|
|
||||||
/// </summary>
|
|
||||||
public interface ISecondaryAliasService : ISharedAliasService;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 同时实现多个别名接口的测试服务。
|
|
||||||
/// </summary>
|
|
||||||
public sealed class AliasAwareService : IPrimaryAliasService, ISecondaryAliasService
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 实现优先级的服务
|
|
||||||
/// </summary>
|
|
||||||
public sealed class PrioritizedService : IPrioritizedService, IMixedService
|
|
||||||
{
|
|
||||||
public int Priority { get; set; }
|
|
||||||
public string? Name { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 不实现优先级的服务
|
|
||||||
/// </summary>
|
|
||||||
public sealed class NonPrioritizedService : IMixedService
|
|
||||||
{
|
|
||||||
public string? Name { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
12
GFramework.Core.Tests/Ioc/NonPrioritizedService.cs
Normal file
12
GFramework.Core.Tests/Ioc/NonPrioritizedService.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace GFramework.Core.Tests.Ioc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 不实现优先级的服务
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NonPrioritizedService : IMixedService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置服务名称
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
17
GFramework.Core.Tests/Ioc/PrioritizedService.cs
Normal file
17
GFramework.Core.Tests/Ioc/PrioritizedService.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace GFramework.Core.Tests.Ioc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现优先级的服务
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PrioritizedService : IPrioritizedService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置优先级
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置服务名称
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
12
GFramework.Core.Tests/Ioc/TestService.cs
Normal file
12
GFramework.Core.Tests/Ioc/TestService.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace GFramework.Core.Tests.Ioc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试服务类,实现 IService 接口
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestService : IService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置优先级
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; set; }
|
||||||
|
}
|
||||||
@ -431,7 +431,11 @@ public class PauseStackManagerTests
|
|||||||
{
|
{
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
var tokens = new List<PauseToken>();
|
var tokens = new List<PauseToken>();
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
var lockObj = new System.Threading.Lock();
|
||||||
|
#else
|
||||||
var lockObj = new object();
|
var lockObj = new object();
|
||||||
|
#endif
|
||||||
|
|
||||||
for (int i = 0; i < 100; i++)
|
for (int i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6,7 +6,6 @@ using GFramework.Core.Environment;
|
|||||||
using GFramework.Core.Events;
|
using GFramework.Core.Events;
|
||||||
using GFramework.Core.Ioc;
|
using GFramework.Core.Ioc;
|
||||||
using GFramework.Core.Query;
|
using GFramework.Core.Query;
|
||||||
using GFramework.Cqrs.Abstractions.Cqrs.Query;
|
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Query;
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
@ -236,179 +235,3 @@ public class AbstractAsyncQueryTests
|
|||||||
Assert.That(result2, Is.EqualTo(40));
|
Assert.That(result2, Is.EqualTo(40));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试用异步查询输入类V2
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TestAsyncQueryInputV2 : IQueryInput
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置值
|
|
||||||
/// </summary>
|
|
||||||
public int Value { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 整数类型测试异步查询类V4,继承AbstractAsyncQuery
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TestAsyncQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化TestAsyncQueryV4的新实例
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">查询输入参数</param>
|
|
||||||
public TestAsyncQueryV4(TestAsyncQueryInputV2 input) : base(input)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取查询是否已执行
|
|
||||||
/// </summary>
|
|
||||||
public bool Executed { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 执行异步查询操作的具体实现
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">查询输入参数</param>
|
|
||||||
/// <returns>查询结果,将输入值乘以2</returns>
|
|
||||||
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
|
|
||||||
{
|
|
||||||
Executed = true;
|
|
||||||
return Task.FromResult(input.Value * 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 字符串类型测试异步查询类V4,继承AbstractAsyncQuery
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TestAsyncStringQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, string>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化TestAsyncStringQueryV4的新实例
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">查询输入参数</param>
|
|
||||||
public TestAsyncStringQueryV4(TestAsyncQueryInputV2 input) : base(input)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取查询是否已执行
|
|
||||||
/// </summary>
|
|
||||||
public bool Executed { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 执行异步查询操作的具体实现
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">查询输入参数</param>
|
|
||||||
/// <returns>格式化的字符串结果</returns>
|
|
||||||
protected override Task<string> OnDoAsync(TestAsyncQueryInputV2 input)
|
|
||||||
{
|
|
||||||
Executed = true;
|
|
||||||
return Task.FromResult($"Value: {input.Value * 2}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 复杂对象类型测试异步查询类V4,继承AbstractAsyncQuery
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TestAsyncComplexQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, TestAsyncQueryResultV2>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化TestAsyncComplexQueryV4的新实例
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">查询输入参数</param>
|
|
||||||
public TestAsyncComplexQueryV4(TestAsyncQueryInputV2 input) : base(input)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取查询是否已执行
|
|
||||||
/// </summary>
|
|
||||||
public bool Executed { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 执行异步查询操作的具体实现
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">查询输入参数</param>
|
|
||||||
/// <returns>复杂对象查询结果</returns>
|
|
||||||
protected override Task<TestAsyncQueryResultV2> OnDoAsync(TestAsyncQueryInputV2 input)
|
|
||||||
{
|
|
||||||
Executed = true;
|
|
||||||
var result = new TestAsyncQueryResultV2
|
|
||||||
{
|
|
||||||
Value = input.Value * 2,
|
|
||||||
DoubleValue = input.Value * 3
|
|
||||||
};
|
|
||||||
return Task.FromResult(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试用异步查询类(抛出异常)
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TestAsyncQueryWithExceptionV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化TestAsyncQueryWithExceptionV4的新实例
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">查询输入参数</param>
|
|
||||||
public TestAsyncQueryWithExceptionV4(TestAsyncQueryInputV2 input) : base(input)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 执行异步查询操作并抛出异常
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">查询输入参数</param>
|
|
||||||
/// <exception cref="InvalidOperationException">总是抛出异常</exception>
|
|
||||||
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Test exception");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试用异步查询子类V4,继承AbstractAsyncQuery
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TestAsyncQueryChildV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化TestAsyncQueryChildV4的新实例
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">查询输入参数</param>
|
|
||||||
public TestAsyncQueryChildV4(TestAsyncQueryInputV2 input) : base(input)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取查询是否已执行
|
|
||||||
/// </summary>
|
|
||||||
public bool Executed { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 执行异步查询操作的具体实现(子类实现,乘以3)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input">查询输入参数</param>
|
|
||||||
/// <returns>查询结果,将输入值乘以3</returns>
|
|
||||||
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
|
|
||||||
{
|
|
||||||
Executed = true;
|
|
||||||
return Task.FromResult(input.Value * 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试用复杂查询结果类V2
|
|
||||||
/// </summary>
|
|
||||||
public sealed class TestAsyncQueryResultV2
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置值
|
|
||||||
/// </summary>
|
|
||||||
public int Value { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置双倍值
|
|
||||||
/// </summary>
|
|
||||||
public int DoubleValue { get; init; }
|
|
||||||
}
|
|
||||||
|
|||||||
38
GFramework.Core.Tests/Query/TestAsyncComplexQueryV4.cs
Normal file
38
GFramework.Core.Tests/Query/TestAsyncComplexQueryV4.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using GFramework.Core.Query;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复杂对象类型测试异步查询类V4,继承AbstractAsyncQuery
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestAsyncComplexQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, TestAsyncQueryResultV2>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化TestAsyncComplexQueryV4的新实例
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询输入参数</param>
|
||||||
|
public TestAsyncComplexQueryV4(TestAsyncQueryInputV2 input) : base(input)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取查询是否已执行
|
||||||
|
/// </summary>
|
||||||
|
public bool Executed { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行异步查询操作的具体实现
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询输入参数</param>
|
||||||
|
/// <returns>复杂对象查询结果</returns>
|
||||||
|
protected override Task<TestAsyncQueryResultV2> OnDoAsync(TestAsyncQueryInputV2 input)
|
||||||
|
{
|
||||||
|
Executed = true;
|
||||||
|
var result = new TestAsyncQueryResultV2
|
||||||
|
{
|
||||||
|
Value = input.Value * 2,
|
||||||
|
DoubleValue = input.Value * 3
|
||||||
|
};
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
GFramework.Core.Tests/Query/TestAsyncQueryChildV4.cs
Normal file
33
GFramework.Core.Tests/Query/TestAsyncQueryChildV4.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using GFramework.Core.Query;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用异步查询子类V4,继承AbstractAsyncQuery
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestAsyncQueryChildV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化TestAsyncQueryChildV4的新实例
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询输入参数</param>
|
||||||
|
public TestAsyncQueryChildV4(TestAsyncQueryInputV2 input) : base(input)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取查询是否已执行
|
||||||
|
/// </summary>
|
||||||
|
public bool Executed { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行异步查询操作的具体实现(子类实现,乘以3)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询输入参数</param>
|
||||||
|
/// <returns>查询结果,将输入值乘以3</returns>
|
||||||
|
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
|
||||||
|
{
|
||||||
|
Executed = true;
|
||||||
|
return Task.FromResult(input.Value * 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
GFramework.Core.Tests/Query/TestAsyncQueryInputV2.cs
Normal file
14
GFramework.Core.Tests/Query/TestAsyncQueryInputV2.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using GFramework.Cqrs.Abstractions.Cqrs.Query;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用异步查询输入类V2
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestAsyncQueryInputV2 : IQueryInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置值
|
||||||
|
/// </summary>
|
||||||
|
public int Value { get; init; }
|
||||||
|
}
|
||||||
17
GFramework.Core.Tests/Query/TestAsyncQueryResultV2.cs
Normal file
17
GFramework.Core.Tests/Query/TestAsyncQueryResultV2.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用复杂查询结果类V2
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestAsyncQueryResultV2
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置值
|
||||||
|
/// </summary>
|
||||||
|
public int Value { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置双倍值
|
||||||
|
/// </summary>
|
||||||
|
public int DoubleValue { get; init; }
|
||||||
|
}
|
||||||
33
GFramework.Core.Tests/Query/TestAsyncQueryV4.cs
Normal file
33
GFramework.Core.Tests/Query/TestAsyncQueryV4.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using GFramework.Core.Query;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 整数类型测试异步查询类V4,继承AbstractAsyncQuery
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestAsyncQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化TestAsyncQueryV4的新实例
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询输入参数</param>
|
||||||
|
public TestAsyncQueryV4(TestAsyncQueryInputV2 input) : base(input)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取查询是否已执行
|
||||||
|
/// </summary>
|
||||||
|
public bool Executed { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行异步查询操作的具体实现
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询输入参数</param>
|
||||||
|
/// <returns>查询结果,将输入值乘以2</returns>
|
||||||
|
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
|
||||||
|
{
|
||||||
|
Executed = true;
|
||||||
|
return Task.FromResult(input.Value * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
GFramework.Core.Tests/Query/TestAsyncQueryWithExceptionV4.cs
Normal file
28
GFramework.Core.Tests/Query/TestAsyncQueryWithExceptionV4.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using GFramework.Core.Query;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用异步查询类(抛出异常)
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestAsyncQueryWithExceptionV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, int>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化TestAsyncQueryWithExceptionV4的新实例
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询输入参数</param>
|
||||||
|
public TestAsyncQueryWithExceptionV4(TestAsyncQueryInputV2 input) : base(input)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行异步查询操作并抛出异常
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询输入参数</param>
|
||||||
|
/// <returns>返回一个不会正常完成的 <see cref="Task{TResult}" />,因为该方法始终抛出异常。</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">总是抛出异常</exception>
|
||||||
|
protected override Task<int> OnDoAsync(TestAsyncQueryInputV2 input)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Test exception");
|
||||||
|
}
|
||||||
|
}
|
||||||
33
GFramework.Core.Tests/Query/TestAsyncStringQueryV4.cs
Normal file
33
GFramework.Core.Tests/Query/TestAsyncStringQueryV4.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using GFramework.Core.Query;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Query;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 字符串类型测试异步查询类V4,继承AbstractAsyncQuery
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestAsyncStringQueryV4 : AbstractAsyncQuery<TestAsyncQueryInputV2, string>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化TestAsyncStringQueryV4的新实例
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询输入参数</param>
|
||||||
|
public TestAsyncStringQueryV4(TestAsyncQueryInputV2 input) : base(input)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取查询是否已执行
|
||||||
|
/// </summary>
|
||||||
|
public bool Executed { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行异步查询操作的具体实现
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询输入参数</param>
|
||||||
|
/// <returns>格式化的字符串结果</returns>
|
||||||
|
protected override Task<string> OnDoAsync(TestAsyncQueryInputV2 input)
|
||||||
|
{
|
||||||
|
Executed = true;
|
||||||
|
return Task.FromResult($"Value: {input.Value * 2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -40,7 +40,13 @@ public class ConfigurationManager : IConfigurationManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用于保护监听器列表的锁
|
/// 用于保护监听器列表的锁
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _watcherLock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _watcherLock = new();
|
private readonly object _watcherLock = new();
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 配置监听器字典(线程安全)
|
/// 配置监听器字典(线程安全)
|
||||||
|
|||||||
@ -12,7 +12,13 @@ internal sealed class CoroutineStatistics : ICoroutineStatistics
|
|||||||
{
|
{
|
||||||
private readonly Dictionary<CoroutinePriority, int> _countByPriority = new();
|
private readonly Dictionary<CoroutinePriority, int> _countByPriority = new();
|
||||||
private readonly Dictionary<string, int> _countByTag = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, int> _countByTag = new(StringComparer.Ordinal);
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
private int _activeCount;
|
private int _activeCount;
|
||||||
private double _maxExecutionTimeMs;
|
private double _maxExecutionTimeMs;
|
||||||
private int _pausedCount;
|
private int _pausedCount;
|
||||||
|
|||||||
@ -10,7 +10,13 @@ namespace GFramework.Core.Events;
|
|||||||
public sealed class EventStatistics : IEventStatistics
|
public sealed class EventStatistics : IEventStatistics
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, int> _listenerCountByType = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, int> _listenerCountByType = new(StringComparer.Ordinal);
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
private readonly Dictionary<string, long> _publishCountByType = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, long> _publishCountByType = new(StringComparer.Ordinal);
|
||||||
private long _totalFailed;
|
private long _totalFailed;
|
||||||
private long _totalHandled;
|
private long _totalHandled;
|
||||||
|
|||||||
@ -10,7 +10,13 @@ namespace GFramework.Core.Events;
|
|||||||
public sealed class FilterableEvent<T>
|
public sealed class FilterableEvent<T>
|
||||||
{
|
{
|
||||||
private readonly List<IEventFilter<T>> _filters = new();
|
private readonly List<IEventFilter<T>> _filters = new();
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
private readonly EventStatistics? _statistics;
|
private readonly EventStatistics? _statistics;
|
||||||
private Action<T>? _onEvent;
|
private Action<T>? _onEvent;
|
||||||
|
|
||||||
@ -152,4 +158,4 @@ public sealed class FilterableEvent<T>
|
|||||||
var count = _onEvent?.GetInvocationList().Length ?? 0;
|
var count = _onEvent?.GetInvocationList().Length ?? 0;
|
||||||
_statistics.UpdateListenerCount(typeof(T).Name, count);
|
_statistics.UpdateListenerCount(typeof(T).Name, count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,13 @@ public class PriorityEvent<T> : IEvent
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 保护处理器集合的并发访问
|
/// 保护处理器集合的并发访问
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _syncRoot = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _syncRoot = new();
|
private readonly object _syncRoot = new();
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 标记事件是否已被处理(用于 UntilHandled 传播模式)
|
/// 标记事件是否已被处理(用于 UntilHandled 传播模式)
|
||||||
@ -326,4 +332,4 @@ public class PriorityEvent<T> : IEvent
|
|||||||
public Action<EventContext<T>> Handler { get; } = handler;
|
public Action<EventContext<T>> Handler { get; } = handler;
|
||||||
public int Priority { get; } = priority;
|
public int Priority { get; } = priority;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,13 @@ namespace GFramework.Core.Events;
|
|||||||
/// <typeparam name="T">事件数据类型</typeparam>
|
/// <typeparam name="T">事件数据类型</typeparam>
|
||||||
public sealed class WeakEvent<T>
|
public sealed class WeakEvent<T>
|
||||||
{
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
private readonly EventStatistics? _statistics;
|
private readonly EventStatistics? _statistics;
|
||||||
private readonly List<WeakReference<Action<T>>> _weakHandlers = new();
|
private readonly List<WeakReference<Action<T>>> _weakHandlers = new();
|
||||||
|
|
||||||
@ -151,4 +157,4 @@ public sealed class WeakEvent<T>
|
|||||||
var count = _weakHandlers.Count(wr => wr.TryGetTarget(out _));
|
var count = _weakHandlers.Count(wr => wr.TryGetTarget(out _));
|
||||||
_statistics.UpdateListenerCount(typeof(T).Name, count);
|
_statistics.UpdateListenerCount(typeof(T).Name, count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,13 @@ public sealed class FileAppender : ILogAppender, IDisposable
|
|||||||
private readonly string _filePath;
|
private readonly string _filePath;
|
||||||
private readonly ILogFilter? _filter;
|
private readonly ILogFilter? _filter;
|
||||||
private readonly ILogFormatter _formatter;
|
private readonly ILogFormatter _formatter;
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private StreamWriter? _writer;
|
private StreamWriter? _writer;
|
||||||
|
|
||||||
@ -114,4 +120,4 @@ public sealed class FileAppender : ILogAppender, IDisposable
|
|||||||
AutoFlush = true
|
AutoFlush = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,13 @@ public sealed class RollingFileAppender : ILogAppender, IDisposable
|
|||||||
private readonly string _baseFilePath;
|
private readonly string _baseFilePath;
|
||||||
private readonly ILogFilter? _filter;
|
private readonly ILogFilter? _filter;
|
||||||
private readonly ILogFormatter _formatter;
|
private readonly ILogFormatter _formatter;
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
private readonly int _maxFileCount;
|
private readonly int _maxFileCount;
|
||||||
private readonly long _maxFileSize;
|
private readonly long _maxFileSize;
|
||||||
private long _currentSize;
|
private long _currentSize;
|
||||||
@ -205,4 +211,4 @@ public sealed class RollingFileAppender : ILogAppender, IDisposable
|
|||||||
// 获取当前文件大小
|
// 获取当前文件大小
|
||||||
_currentSize = File.Exists(_baseFilePath) ? new FileInfo(_baseFilePath).Length : 0;
|
_currentSize = File.Exists(_baseFilePath) ? new FileInfo(_baseFilePath).Length : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,7 +91,13 @@ public sealed class SamplingFilter : ILogFilter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private sealed class SamplingState
|
private sealed class SamplingState
|
||||||
{
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
private readonly ITimeProvider _timeProvider;
|
private readonly ITimeProvider _timeProvider;
|
||||||
private long _count;
|
private long _count;
|
||||||
private long _lastAccessTicks;
|
private long _lastAccessTicks;
|
||||||
|
|||||||
@ -12,9 +12,15 @@ namespace GFramework.Core.Property;
|
|||||||
public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<T>
|
public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<T>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用于保护委托链和值访问的锁对象
|
/// 用于保护委托链和值访问的同步原语
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 属性值变化事件回调委托,当属性值发生变化时被调用
|
/// 属性值变化事件回调委托,当属性值发生变化时被调用
|
||||||
@ -172,4 +178,4 @@ public class BindableProperty<T>(T defaultValue = default!) : IBindableProperty<
|
|||||||
{
|
{
|
||||||
return Value?.ToString() ?? string.Empty;
|
return Value?.ToString() ?? string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,13 @@ internal sealed class ResourceCache
|
|||||||
private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace.";
|
private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace.";
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, ResourceCacheEntry> _cache = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ResourceCacheEntry> _cache = new(StringComparer.Ordinal);
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取已缓存资源的数量
|
/// 获取已缓存资源的数量
|
||||||
|
|||||||
@ -11,7 +11,13 @@ namespace GFramework.Core.Resource;
|
|||||||
/// <typeparam name="T">资源类型</typeparam>
|
/// <typeparam name="T">资源类型</typeparam>
|
||||||
internal sealed class ResourceHandle<T> : IResourceHandle<T> where T : class
|
internal sealed class ResourceHandle<T> : IResourceHandle<T> where T : class
|
||||||
{
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ResourceHandle<T>));
|
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ResourceHandle<T>));
|
||||||
private readonly Action<string> _onDispose;
|
private readonly Action<string> _onDispose;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
@ -141,4 +147,4 @@ internal sealed class ResourceHandle<T> : IResourceHandle<T> where T : class
|
|||||||
_logger.Error($"[ResourceHandle] Error disposing resource '{Path}': {ex.Message}");
|
_logger.Error($"[ResourceHandle] Error disposing resource '{Path}': {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,13 @@ public class ResourceManager : IResourceManager
|
|||||||
|
|
||||||
private readonly ResourceCache _cache = new();
|
private readonly ResourceCache _cache = new();
|
||||||
private readonly ConcurrentDictionary<Type, object> _loaders = new();
|
private readonly ConcurrentDictionary<Type, object> _loaders = new();
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _loadLock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _loadLock = new();
|
private readonly object _loadLock = new();
|
||||||
|
#endif
|
||||||
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ResourceManager));
|
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ResourceManager));
|
||||||
private IResourceReleaseStrategy _releaseStrategy;
|
private IResourceReleaseStrategy _releaseStrategy;
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,13 @@ namespace GFramework.Core.State;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
||||||
{
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _lock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
#endif
|
||||||
|
|
||||||
private readonly HashSet<IState> _registeredStates = [];
|
private readonly HashSet<IState> _registeredStates = [];
|
||||||
private readonly Stack<IState> _stateHistory = new();
|
private readonly Stack<IState> _stateHistory = new();
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# GFramework.Cqrs.Abstractions
|
# GFramework.Cqrs.Abstractions
|
||||||
|
|
||||||
`GFramework.Cqrs.Abstractions` 提供 GFramework CQRS 的最小契约层。它只包含消息接口、处理器接口、运行时 seam 和管道契约,不包含默认 dispatcher、处理器扫描或任何 `GFramework.Core` 运行时实现。适合以下场景:
|
`GFramework.Cqrs.Abstractions` 提供 GFramework CQRS 的最小契约层。它只包含消息接口、处理器接口、运行时协作接口和管道契约,不包含默认 dispatcher、处理器扫描或任何 `GFramework.Core` 运行时实现。适合以下场景:
|
||||||
|
|
||||||
- 你的业务程序集只需要声明 Command、Query、Notification、Stream Request 或处理器接口。
|
- 你的业务程序集只需要声明 Command、Query、Notification、Stream Request 或处理器接口。
|
||||||
- 你希望把消息契约放在更稳定的基础层,避免直接依赖默认 runtime 实现。
|
- 你希望把消息契约放在更稳定的基础层,避免直接依赖默认 runtime 实现。
|
||||||
@ -43,7 +43,7 @@
|
|||||||
- `Cqrs/IRequestHandler.cs`
|
- `Cqrs/IRequestHandler.cs`
|
||||||
- `Cqrs/INotificationHandler.cs`
|
- `Cqrs/INotificationHandler.cs`
|
||||||
- `Cqrs/IStreamRequestHandler.cs`
|
- `Cqrs/IStreamRequestHandler.cs`
|
||||||
- 运行时 seam
|
- 运行时协作接口
|
||||||
- `Cqrs/ICqrsRuntime.cs`
|
- `Cqrs/ICqrsRuntime.cs`
|
||||||
- `Cqrs/ICqrsContext.cs`
|
- `Cqrs/ICqrsContext.cs`
|
||||||
- `Cqrs/ICqrsHandlerRegistrar.cs`
|
- `Cqrs/ICqrsHandlerRegistrar.cs`
|
||||||
@ -93,7 +93,7 @@ public sealed class GetPlayerProfileHandler
|
|||||||
|
|
||||||
- 只引用本包时,没有 `CommandBase<TInput, TResponse>`、`QueryBase<TInput, TResponse>`、`NotificationBase<TInput>` 等消息基类。
|
- 只引用本包时,没有 `CommandBase<TInput, TResponse>`、`QueryBase<TInput, TResponse>`、`NotificationBase<TInput>` 等消息基类。
|
||||||
- 只引用本包时,没有 `AbstractCommandHandler`、`AbstractQueryHandler`、`AbstractNotificationHandler` 等处理器基类。
|
- 只引用本包时,没有 `AbstractCommandHandler`、`AbstractQueryHandler`、`AbstractNotificationHandler` 等处理器基类。
|
||||||
- `ICqrsContext` 当前是轻量 marker seam;默认 runtime 在需要向 `IContextAware` 处理器注入上下文时,仍要求传入的上下文同时实现 `IArchitectureContext`。
|
- `ICqrsContext` 当前是轻量 marker 接口;默认 runtime 在需要向 `IContextAware` 处理器注入上下文时,仍要求传入的上下文同时实现 `IArchitectureContext`。
|
||||||
|
|
||||||
## 文档入口
|
## 文档入口
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,13 @@ internal sealed class WeakKeyCache<TKey, TValue>
|
|||||||
where TKey : class
|
where TKey : class
|
||||||
where TValue : class
|
where TValue : class
|
||||||
{
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _gate = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
|
#endif
|
||||||
private ConditionalWeakTable<TKey, TValue> _entries = new();
|
private ConditionalWeakTable<TKey, TValue> _entries = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -37,11 +37,11 @@
|
|||||||
|
|
||||||
下表汇总当前契约包的类型级 XML 文档入口,方便把 README、站内抽象页与源码阅读顺序对齐。
|
下表汇总当前契约包的类型级 XML 文档入口,方便把 README、站内抽象页与源码阅读顺序对齐。
|
||||||
|
|
||||||
| 类型族 | 代表类型 | XML 状态 | 阅读重点 |
|
| 类型族 | 代表类型 | 阅读重点 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 模块契约 | `IArchEcsModule` | 已覆盖 | 宿主循环如何统一驱动 ECS 更新 |
|
| 模块契约 | `IArchEcsModule` | 宿主循环如何统一驱动 ECS 更新 |
|
||||||
| 系统桥接契约 | `IArchSystemAdapter<T>` | 已覆盖 | 外部模块怎样只依赖更新接口而不绑定默认实现 |
|
| 系统桥接契约 | `IArchSystemAdapter<T>` | 外部模块怎样只依赖更新接口而不绑定默认实现 |
|
||||||
| 配置对象 | `ArchOptions` | 已覆盖 | 跨程序集共享 ECS 配置边界 |
|
| 配置对象 | `ArchOptions` | 跨程序集共享 ECS 配置边界 |
|
||||||
|
|
||||||
## 最小接入路径
|
## 最小接入路径
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# GFramework.Ecs.Arch
|
# GFramework.Ecs.Arch
|
||||||
|
|
||||||
`GFramework.Ecs.Arch` 是 `GFramework` 当前 Arch ECS family 的默认运行时实现包。
|
`GFramework.Ecs.Arch` 是 `GFramework` 当前 Arch ECS 集成的默认运行时实现包。
|
||||||
|
|
||||||
它负责把 Arch `World`、GFramework 的服务模块生命周期,以及 `ArchSystemAdapter<T>` 系统桥接到同一条采用路径中。
|
它负责把 Arch `World`、GFramework 的服务模块生命周期,以及 `ArchSystemAdapter<T>` 系统桥接到同一条采用路径中。
|
||||||
如果你需要的只是共享契约,请改为依赖 `GFramework.Ecs.Arch.Abstractions`。
|
如果你需要的只是共享契约,请改为依赖 `GFramework.Ecs.Arch.Abstractions`。
|
||||||
|
|||||||
@ -25,8 +25,7 @@
|
|||||||
- `FileStorage`、`ScopedStorage`、`JsonSerializer`、`SettingsModel<TRepository>`、`SaveRepository<TSaveData>`、`SceneRouterBase`、`UiRouterBase`、`YamlConfigLoader` 等都在实现这里的契约。
|
- `FileStorage`、`ScopedStorage`、`JsonSerializer`、`SettingsModel<TRepository>`、`SaveRepository<TSaveData>`、`SceneRouterBase`、`UiRouterBase`、`YamlConfigLoader` 等都在实现这里的契约。
|
||||||
- 引擎适配包或项目代码
|
- 引擎适配包或项目代码
|
||||||
- `IUiFactory`、`ISceneFactory`、`IUiRoot`、`ISceneRoot`、资源注册表等通常由引擎适配层或游戏项目自己实现。
|
- `IUiFactory`、`ISceneFactory`、`IUiRoot`、`ISceneRoot`、资源注册表等通常由引擎适配层或游戏项目自己实现。
|
||||||
- 仓库内 `ai-libs/` 下的只读参考实现通常也是这样组织:页面 / 场景 factory、root、registry 在项目层,
|
- 常见做法也是这样组织:页面 / 场景 factory、root、registry 在项目层,运行时基类和契约来自 `GFramework.Game` 与本包。
|
||||||
运行时基类和契约来自 `GFramework.Game` 与本包。
|
|
||||||
|
|
||||||
## 子系统地图
|
## 子系统地图
|
||||||
|
|
||||||
@ -133,17 +132,16 @@ Scene 与 UI 路由共享这套基础约定。
|
|||||||
|
|
||||||
## XML 阅读入口
|
## XML 阅读入口
|
||||||
|
|
||||||
下面这份目录视图汇总了 `2026-04-23` 可直接对照的 `GFramework.Game.Abstractions` 类型级 XML 文档入口:只统计公开 /
|
下面这份目录视图用于帮助你建立 `GFramework.Game.Abstractions` 的契约层阅读顺序;更细的参数、返回值、异常和生命周期说明,建议继续回到具体类型与成员确认。
|
||||||
内部类型声明是否带 XML 注释,用来帮助你建立契约层阅读顺序;更细的参数、返回值、异常和生命周期说明,建议继续回到具体类型与成员确认。
|
|
||||||
|
|
||||||
| 契约族 | 基线状态 | 代表类型 | 阅读重点 |
|
| 契约族 | 代表类型 | 阅读重点 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `Config/` | `7/7` 个类型声明已带 XML 注释 | `IConfigLoader`、`IConfigRegistry`、`IConfigTable<TKey, TValue>`、`ConfigLoadException` | 看配置表注册、读取约定和失败诊断模型 |
|
| `Config/` | `IConfigLoader`、`IConfigRegistry`、`IConfigTable<TKey, TValue>`、`ConfigLoadException` | 看配置表注册、读取约定和失败诊断模型 |
|
||||||
| `Data/` | `14/14` 个类型声明已带 XML 注释 | `IDataRepository`、`ISettingsDataRepository`、`ISaveRepository<TSaveData>`、`DataRepositoryOptions` | 看业务数据、设置持久化、槽位存档和版本迁移契约 |
|
| `Data/` | `IDataRepository`、`ISettingsDataRepository`、`ISaveRepository<TSaveData>`、`DataRepositoryOptions` | 看业务数据、设置持久化、槽位存档和版本迁移契约 |
|
||||||
| `Setting/` | `12/12` 个类型声明已带 XML 注释 | `ISettingsData`、`ISettingsModel`、`ISettingsSystem`、`LocalizationSettings` | 看设置数据、应用语义、迁移接口和内置设置对象 |
|
| `Setting/` | `ISettingsData`、`ISettingsModel`、`ISettingsSystem`、`LocalizationSettings` | 看设置数据、应用语义、迁移接口和内置设置对象 |
|
||||||
| `Scene/` | `14/14` 个类型声明已带 XML 注释 | `IScene`、`ISceneRouter`、`ISceneFactory`、`SceneTransitionEvent` | 看场景行为、路由、工厂 / root 边界与转场事件模型 |
|
| `Scene/` | `IScene`、`ISceneRouter`、`ISceneFactory`、`SceneTransitionEvent` | 看场景行为、路由、工厂 / root 边界与转场事件模型 |
|
||||||
| `UI/` | `19/19` 个类型声明已带 XML 注释 | `IUiPage`、`IUiRouter`、`IUiFactory`、`UiInteractionProfile`、`UiTransitionHandlerOptions` | 看页面栈、层级 UI、输入动作与 UI 转场契约 |
|
| `UI/` | `IUiPage`、`IUiRouter`、`IUiFactory`、`UiInteractionProfile`、`UiTransitionHandlerOptions` | 看页面栈、层级 UI、输入动作与 UI 转场契约 |
|
||||||
| `Routing/` `Storage/` `Asset/` `Enums/` | `13/13` 个类型声明已带 XML 注释 | `IRoute`、`IRouteContext`、`IFileStorage`、`IAssetRegistry<T>`、`UiLayer`、`SceneTransitionType` | 看公共路由上下文、存储角色、资源注册表与跨层共享枚举 |
|
| `Routing/` `Storage/` `Asset/` `Enums/` | `IRoute`、`IRouteContext`、`IFileStorage`、`IAssetRegistry<T>`、`UiLayer`、`SceneTransitionType` | 看公共路由上下文、存储角色、资源注册表与跨层共享枚举 |
|
||||||
|
|
||||||
## 最小接入路径
|
## 最小接入路径
|
||||||
|
|
||||||
@ -210,9 +208,9 @@ public sealed class ContinueGameCommandHandler
|
|||||||
|
|
||||||
也就是说,本包回答的是“项目各层如何约定”,`GFramework.Game` 回答的是“这些约定默认怎么跑起来”。
|
也就是说,本包回答的是“项目各层如何约定”,`GFramework.Game` 回答的是“这些约定默认怎么跑起来”。
|
||||||
|
|
||||||
## `ai-libs/` 里的参考接入线索
|
## 典型分层方式
|
||||||
|
|
||||||
`ai-libs/` 下的只读参考实现对本包的使用方式,能比较清楚地说明它的职责边界:
|
典型项目对本包的使用方式,通常能清楚体现它的职责边界:
|
||||||
|
|
||||||
- 公共脚本广泛引用:
|
- 公共脚本广泛引用:
|
||||||
- `IUiRouter`
|
- `IUiRouter`
|
||||||
|
|||||||
@ -46,12 +46,12 @@ GameProject/
|
|||||||
|
|
||||||
## XML 阅读入口
|
## XML 阅读入口
|
||||||
|
|
||||||
下面这份目录视图汇总了 `2026-04-23` 可直接对照的 `GFramework.Game.SourceGenerators` 类型级 XML 文档入口:只统计公开类型声明是否带 XML 注释,用来帮助你定位生成器入口;具体诊断消息、生成输出和兼容性语义仍建议回到源码与测试继续核对。
|
下面这份目录视图用于帮助你定位 `GFramework.Game.SourceGenerators` 的生成器入口;具体诊断消息、生成输出和兼容性语义仍建议回到源码与测试继续核对。
|
||||||
|
|
||||||
| 类型族 | 基线状态 | 代表类型 | 阅读重点 |
|
| 阅读主题 | 代表类型 | 阅读重点 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `Config/` | `1/1` 个类型声明已带 XML 注释 | `SchemaConfigGenerator` | 看 schema 到配置类型 / 表包装 / 注册辅助代码的生成入口 |
|
| 配置生成入口 | `SchemaConfigGenerator` | 看 schema 到配置类型 / 表包装 / 注册辅助代码的生成入口 |
|
||||||
| `Diagnostics/` | `1/1` 个类型声明已带 XML 注释 | `ConfigSchemaDiagnostics` | 看生成器会抛出的诊断类别与失败边界 |
|
| 诊断与失败边界 | `ConfigSchemaDiagnostics` | 看生成器会抛出的诊断类别与失败边界 |
|
||||||
|
|
||||||
## 最小接入路径
|
## 最小接入路径
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
using GFramework.Core.Abstractions.Events;
|
using GFramework.Core.Abstractions.Events;
|
||||||
using GFramework.Game.Abstractions.Config;
|
using GFramework.Game.Abstractions.Config;
|
||||||
using GFramework.Game.Config;
|
using GFramework.Game.Config;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
namespace GFramework.Game.Tests.Config;
|
namespace GFramework.Game.Tests.Config;
|
||||||
|
|
||||||
@ -2784,6 +2787,130 @@ public class YamlConfigLoaderTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证底层文件读取在取消时会保留 <see cref="OperationCanceledException" />,
|
||||||
|
/// 避免热重载把会话级取消误报为配置读取失败。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void ReadYamlAsync_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested()
|
||||||
|
{
|
||||||
|
CreateConfigFile(
|
||||||
|
"monster/slime.yaml",
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Slime
|
||||||
|
hp: 10
|
||||||
|
""");
|
||||||
|
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
|
||||||
|
var registration = GetSingleYamlTableRegistration(loader);
|
||||||
|
var readYamlAsyncMethod = registration.GetType()
|
||||||
|
.GetMethod("ReadYamlAsync", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.That(readYamlAsyncMethod, Is.Not.Null);
|
||||||
|
|
||||||
|
using var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
cancellationTokenSource.Cancel();
|
||||||
|
|
||||||
|
// 通过反射直接命中注册项的文件读取路径,稳定回归本次取消语义修复。
|
||||||
|
var readTask = (Task<string>)readYamlAsyncMethod!.Invoke(
|
||||||
|
registration,
|
||||||
|
new object?[]
|
||||||
|
{
|
||||||
|
Path.Combine(_rootPath, "monster"),
|
||||||
|
Path.Combine(_rootPath, "monster", "slime.yaml"),
|
||||||
|
null,
|
||||||
|
cancellationTokenSource.Token
|
||||||
|
})!;
|
||||||
|
|
||||||
|
Assert.That(
|
||||||
|
async () => await readTask.ConfigureAwait(false),
|
||||||
|
Throws.InstanceOf<OperationCanceledException>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证同步反序列化阶段遇到已取消 token 时会直接透传 <see cref="OperationCanceledException" />,
|
||||||
|
/// 避免把停止加载误报为 YAML 解析失败。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void DeserializeValue_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested()
|
||||||
|
{
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
|
||||||
|
var registration = GetSingleYamlTableRegistration(loader);
|
||||||
|
var deserializeValueMethod = registration.GetType()
|
||||||
|
.GetMethod("DeserializeValue", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.That(deserializeValueMethod, Is.Not.Null);
|
||||||
|
|
||||||
|
using var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
cancellationTokenSource.Cancel();
|
||||||
|
|
||||||
|
var deserializer = new DeserializerBuilder().Build();
|
||||||
|
var exception = Assert.Throws<TargetInvocationException>(() =>
|
||||||
|
deserializeValueMethod!.Invoke(
|
||||||
|
registration,
|
||||||
|
new object?[]
|
||||||
|
{
|
||||||
|
deserializer,
|
||||||
|
Path.Combine(_rootPath, "monster"),
|
||||||
|
Path.Combine(_rootPath, "monster", "slime.yaml"),
|
||||||
|
null,
|
||||||
|
"""
|
||||||
|
id: 1
|
||||||
|
name: Slime
|
||||||
|
hp: 10
|
||||||
|
""",
|
||||||
|
cancellationTokenSource.Token
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 反射调用同步私有方法时会把原始异常包装为 TargetInvocationException。
|
||||||
|
Assert.That(exception!.InnerException, Is.InstanceOf<OperationCanceledException>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证构建最终配置表阶段遇到已取消 token 时会继续透传 <see cref="OperationCanceledException" />,
|
||||||
|
/// 避免热重载把提交前取消记录成构表失败。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void BuildLoadResult_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested()
|
||||||
|
{
|
||||||
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
|
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
|
||||||
|
var registration = GetSingleYamlTableRegistration(loader);
|
||||||
|
var buildLoadResultMethod = registration.GetType()
|
||||||
|
.GetMethod("BuildLoadResult", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.That(buildLoadResultMethod, Is.Not.Null);
|
||||||
|
|
||||||
|
using var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
cancellationTokenSource.Cancel();
|
||||||
|
|
||||||
|
var exception = Assert.Throws<TargetInvocationException>(() =>
|
||||||
|
buildLoadResultMethod!.Invoke(
|
||||||
|
registration,
|
||||||
|
new object?[]
|
||||||
|
{
|
||||||
|
Path.Combine(_rootPath, "monster"),
|
||||||
|
null,
|
||||||
|
new List<MonsterConfigStub>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Name = "Slime",
|
||||||
|
Hp = 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new List<YamlConfigReferenceUsage>(),
|
||||||
|
cancellationTokenSource.Token
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 反射调用同步私有方法时会把原始异常包装为 TargetInvocationException。
|
||||||
|
Assert.That(exception!.InnerException, Is.InstanceOf<OperationCanceledException>());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证依赖关系仅来自 <c>contains</c> 子 schema 时,热重载仍会追踪该依赖并在目标表破坏引用后回滚。
|
/// 验证依赖关系仅来自 <c>contains</c> 子 schema 时,热重载仍会追踪该依赖并在目标表破坏引用后回滚。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -2928,7 +3055,7 @@ public class YamlConfigLoaderTests
|
|||||||
|
|
||||||
Assert.That(exception!.ParamName, Is.EqualTo("options"));
|
Assert.That(exception!.ParamName, Is.EqualTo("options"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证热重载失败时会保留旧表状态,并通过失败回调暴露诊断信息。
|
/// 验证热重载失败时会保留旧表状态,并通过失败回调暴露诊断信息。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -3372,6 +3499,22 @@ public class YamlConfigLoaderTests
|
|||||||
CreateConfigFile(relativePath, content);
|
CreateConfigFile(relativePath, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static object GetSingleYamlTableRegistration(YamlConfigLoader loader)
|
||||||
|
{
|
||||||
|
var registrationsField = typeof(YamlConfigLoader).GetField(
|
||||||
|
"_registrations",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
Assert.That(registrationsField, Is.Not.Null);
|
||||||
|
|
||||||
|
var registrations = registrationsField!.GetValue(loader) as System.Collections.IList;
|
||||||
|
|
||||||
|
Assert.That(registrations, Is.Not.Null);
|
||||||
|
Assert.That(registrations!.Count, Is.EqualTo(1));
|
||||||
|
|
||||||
|
return registrations[0]!;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 在限定时间内等待异步任务完成,避免文件监听测试无限挂起。
|
/// 在限定时间内等待异步任务完成,避免文件监听测试无限挂起。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -137,12 +137,10 @@ public sealed class SettingsModelTests
|
|||||||
var migrationMapLock = lockField!.GetValue(model);
|
var migrationMapLock = lockField!.GetValue(model);
|
||||||
Assert.That(migrationMapLock, Is.Not.Null);
|
Assert.That(migrationMapLock, Is.Not.Null);
|
||||||
|
|
||||||
Task initializeTask;
|
var tasks = WithSynchronizationLockHeld(migrationMapLock!, () =>
|
||||||
Task registerTask;
|
|
||||||
lock (migrationMapLock!)
|
|
||||||
{
|
{
|
||||||
initializeTask = Task.Run(() => model.InitializeAsync());
|
var initializeTask = Task.Run(() => model.InitializeAsync());
|
||||||
registerTask = Task.Run(() => model.RegisterMigration(new TestLatestSettingsMigrationV2ToV3()));
|
var registerTask = Task.Run(() => model.RegisterMigration(new TestLatestSettingsMigrationV2ToV3()));
|
||||||
|
|
||||||
Thread.Sleep(50);
|
Thread.Sleep(50);
|
||||||
|
|
||||||
@ -151,7 +149,11 @@ public sealed class SettingsModelTests
|
|||||||
Assert.That(initializeTask.IsCompleted, Is.False);
|
Assert.That(initializeTask.IsCompleted, Is.False);
|
||||||
Assert.That(registerTask.IsCompleted, Is.False);
|
Assert.That(registerTask.IsCompleted, Is.False);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
return (initializeTask, registerTask);
|
||||||
|
});
|
||||||
|
|
||||||
|
var (initializeTask, registerTask) = tasks;
|
||||||
|
|
||||||
await Task.WhenAll(initializeTask, registerTask);
|
await Task.WhenAll(initializeTask, registerTask);
|
||||||
|
|
||||||
@ -171,6 +173,35 @@ public sealed class SettingsModelTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 以与被测代码相同的同步原语持有反射获取到的锁对象,避免在 .NET 9+ 上把 <see cref="System.Threading.Lock" />
|
||||||
|
/// 退化成 <see cref="Monitor" /> 语义,导致并发测试误判。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="syncRoot">通过反射读取到的私有锁字段。</param>
|
||||||
|
/// <typeparam name="TResult">持锁代码返回的结果类型。</typeparam>
|
||||||
|
/// <param name="action">持锁期间执行的断言与并发调度逻辑。</param>
|
||||||
|
/// <returns>持锁代码的返回值。</returns>
|
||||||
|
private static TResult WithSynchronizationLockHeld<TResult>(object syncRoot, Func<TResult> action)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(syncRoot);
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
if (syncRoot is System.Threading.Lock typedLock)
|
||||||
|
{
|
||||||
|
using (typedLock.EnterScope())
|
||||||
|
{
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
lock (syncRoot)
|
||||||
|
{
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class TestSettingsData : ISettingsData
|
private sealed class TestSettingsData : ISettingsData
|
||||||
{
|
{
|
||||||
public string Value { get; set; } = "default";
|
public string Value { get; set; } = "default";
|
||||||
|
|||||||
@ -20,7 +20,11 @@ public sealed class GameConfigBootstrap : IDisposable
|
|||||||
|
|
||||||
// All lifecycle transitions share one gate so initialization, hot-reload startup,
|
// All lifecycle transitions share one gate so initialization, hot-reload startup,
|
||||||
// stop, and disposal never publish half-finished state to concurrent callers.
|
// stop, and disposal never publish half-finished state to concurrent callers.
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
private readonly Lock _stateGate = new();
|
||||||
|
#else
|
||||||
private readonly object _stateGate = new();
|
private readonly object _stateGate = new();
|
||||||
|
#endif
|
||||||
private readonly GameConfigBootstrapOptions _options;
|
private readonly GameConfigBootstrapOptions _options;
|
||||||
private IUnRegister? _hotReload;
|
private IUnRegister? _hotReload;
|
||||||
private YamlConfigLoader? _loader;
|
private YamlConfigLoader? _loader;
|
||||||
@ -210,67 +214,16 @@ public sealed class GameConfigBootstrap : IDisposable
|
|||||||
/// </exception>
|
/// </exception>
|
||||||
public void StartHotReload(YamlConfigHotReloadOptions? options = null)
|
public void StartHotReload(YamlConfigHotReloadOptions? options = null)
|
||||||
{
|
{
|
||||||
YamlConfigLoader loader;
|
var loader = BeginHotReloadStart();
|
||||||
lock (_stateGate)
|
|
||||||
{
|
|
||||||
ThrowIfDisposedCore();
|
|
||||||
|
|
||||||
loader = _loader ?? throw new InvalidOperationException(
|
|
||||||
"Hot reload can only be started after the initial config load succeeds.");
|
|
||||||
|
|
||||||
if (_isStartingHotReload || _hotReload != null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Hot reload is already enabled.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_isStartingHotReload = true;
|
|
||||||
_stopHotReloadAfterStart = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
IUnRegister? hotReload = null;
|
IUnRegister? hotReload = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
hotReload = loader.EnableHotReload(Registry, options);
|
hotReload = loader.EnableHotReload(Registry, options);
|
||||||
|
hotReload = CompleteHotReloadStart(hotReload);
|
||||||
var shouldStop = false;
|
|
||||||
lock (_stateGate)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ThrowIfDisposedCore();
|
|
||||||
|
|
||||||
// Stop/Dispose may arrive while the watcher is being created. In that
|
|
||||||
// case, release the new handle immediately instead of publishing it.
|
|
||||||
if (_stopHotReloadAfterStart)
|
|
||||||
{
|
|
||||||
shouldStop = true;
|
|
||||||
_stopHotReloadAfterStart = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_hotReload = hotReload;
|
|
||||||
hotReload = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isStartingHotReload = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldStop)
|
|
||||||
{
|
|
||||||
hotReload?.UnRegister();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
lock (_stateGate)
|
ResetHotReloadStartAfterFailure();
|
||||||
{
|
|
||||||
_isStartingHotReload = false;
|
|
||||||
_stopHotReloadAfterStart = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
hotReload?.UnRegister();
|
hotReload?.UnRegister();
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@ -332,4 +285,70 @@ public sealed class GameConfigBootstrap : IDisposable
|
|||||||
throw new ObjectDisposedException(nameof(GameConfigBootstrap));
|
throw new ObjectDisposedException(nameof(GameConfigBootstrap));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private YamlConfigLoader BeginHotReloadStart()
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
ThrowIfDisposedCore();
|
||||||
|
|
||||||
|
var loader = _loader ?? throw new InvalidOperationException(
|
||||||
|
"Hot reload can only be started after the initial config load succeeds.");
|
||||||
|
|
||||||
|
if (_isStartingHotReload || _hotReload != null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Hot reload is already enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_isStartingHotReload = true;
|
||||||
|
_stopHotReloadAfterStart = false;
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IUnRegister? CompleteHotReloadStart(IUnRegister? hotReload)
|
||||||
|
{
|
||||||
|
var shouldStop = false;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ThrowIfDisposedCore();
|
||||||
|
|
||||||
|
// Stop/Dispose may arrive while the watcher is being created. In that
|
||||||
|
// case, release the new handle immediately instead of publishing it.
|
||||||
|
if (_stopHotReloadAfterStart)
|
||||||
|
{
|
||||||
|
shouldStop = true;
|
||||||
|
_stopHotReloadAfterStart = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_hotReload = hotReload;
|
||||||
|
hotReload = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isStartingHotReload = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldStop)
|
||||||
|
{
|
||||||
|
hotReload?.UnRegister();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hotReload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetHotReloadStartAfterFailure()
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_isStartingHotReload = false;
|
||||||
|
_stopHotReloadAfterStart = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading;
|
||||||
using GFramework.Core.Abstractions.Events;
|
using GFramework.Core.Abstractions.Events;
|
||||||
using GFramework.Game.Abstractions.Config;
|
using GFramework.Game.Abstractions.Config;
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
@ -472,93 +474,178 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
IDeserializer deserializer,
|
IDeserializer deserializer,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var directoryPath = Path.Combine(rootPath, RelativePath);
|
var directoryPath = GetValidatedDirectoryPath(rootPath);
|
||||||
if (!Directory.Exists(directoryPath))
|
var schema = await LoadSchemaAsync(rootPath, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
|
||||||
ConfigLoadFailureKind.ConfigDirectoryNotFound,
|
|
||||||
Name,
|
|
||||||
$"Config directory '{directoryPath}' was not found for table '{Name}'.",
|
|
||||||
configDirectoryPath: directoryPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
YamlConfigSchema? schema = null;
|
|
||||||
IReadOnlyCollection<string> referencedTableNames = Array.Empty<string>();
|
|
||||||
if (!string.IsNullOrEmpty(SchemaRelativePath))
|
|
||||||
{
|
|
||||||
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
|
|
||||||
schema = await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
referencedTableNames = schema.ReferencedTableNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
var referenceUsages = new List<YamlConfigReferenceUsage>();
|
var referenceUsages = new List<YamlConfigReferenceUsage>();
|
||||||
|
var values = await LoadValuesAsync(
|
||||||
|
directoryPath,
|
||||||
|
deserializer,
|
||||||
|
schema,
|
||||||
|
referenceUsages,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return BuildLoadResult(directoryPath, schema, values, referenceUsages, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetValidatedDirectoryPath(string rootPath)
|
||||||
|
{
|
||||||
|
var directoryPath = Path.Combine(rootPath, RelativePath);
|
||||||
|
if (Directory.Exists(directoryPath))
|
||||||
|
{
|
||||||
|
return directoryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.ConfigDirectoryNotFound,
|
||||||
|
Name,
|
||||||
|
$"Config directory '{directoryPath}' was not found for table '{Name}'.",
|
||||||
|
configDirectoryPath: directoryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<YamlConfigSchema?> LoadSchemaAsync(string rootPath, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(SchemaRelativePath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
|
||||||
|
return await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<TValue>> LoadValuesAsync(
|
||||||
|
string directoryPath,
|
||||||
|
IDeserializer deserializer,
|
||||||
|
YamlConfigSchema? schema,
|
||||||
|
List<YamlConfigReferenceUsage> referenceUsages,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
var values = new List<TValue>();
|
var values = new List<TValue>();
|
||||||
var files = Directory
|
foreach (var file in GetYamlFiles(directoryPath))
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var yaml = await ReadYamlAsync(directoryPath, file, schema, cancellationToken).ConfigureAwait(false);
|
||||||
|
CollectReferenceUsages(referenceUsages, schema, file, yaml);
|
||||||
|
values.Add(DeserializeValue(deserializer, directoryPath, file, schema, yaml, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] GetYamlFiles(string directoryPath)
|
||||||
|
{
|
||||||
|
return Directory
|
||||||
.EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly)
|
.EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly)
|
||||||
.Where(static path =>
|
.Where(static path =>
|
||||||
path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) ||
|
path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) ||
|
||||||
path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase))
|
path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase))
|
||||||
.OrderBy(static path => path, StringComparer.Ordinal)
|
.OrderBy(static path => path, StringComparer.Ordinal)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var file in files)
|
private async Task<string> ReadYamlAsync(
|
||||||
{
|
string directoryPath,
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
string file,
|
||||||
|
YamlConfigSchema? schema,
|
||||||
string yaml;
|
CancellationToken cancellationToken)
|
||||||
try
|
{
|
||||||
{
|
|
||||||
yaml = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
|
||||||
ConfigLoadFailureKind.ConfigFileReadFailed,
|
|
||||||
Name,
|
|
||||||
$"Failed to read config file '{file}' for table '{Name}'.",
|
|
||||||
configDirectoryPath: directoryPath,
|
|
||||||
yamlPath: file,
|
|
||||||
schemaPath: schema?.SchemaPath,
|
|
||||||
innerException: exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema != null)
|
|
||||||
{
|
|
||||||
// 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。
|
|
||||||
referenceUsages.AddRange(
|
|
||||||
YamlConfigSchemaValidator.ValidateAndCollectReferences(Name, schema, file, yaml));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var value = deserializer.Deserialize<TValue>(yaml);
|
|
||||||
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("YAML content was deserialized to null.");
|
|
||||||
}
|
|
||||||
|
|
||||||
values.Add(value);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
throw ConfigLoadExceptionFactory.Create(
|
|
||||||
ConfigLoadFailureKind.DeserializationFailed,
|
|
||||||
Name,
|
|
||||||
$"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.",
|
|
||||||
configDirectoryPath: directoryPath,
|
|
||||||
yamlPath: file,
|
|
||||||
schemaPath: schema?.SchemaPath,
|
|
||||||
detail: $"Target CLR type: {typeof(TValue).FullName}.",
|
|
||||||
innerException: exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
return await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// 保留原始取消语义,避免热重载把会话级取消误报为配置读取失败。
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.ConfigFileReadFailed,
|
||||||
|
Name,
|
||||||
|
$"Failed to read config file '{file}' for table '{Name}'.",
|
||||||
|
configDirectoryPath: directoryPath,
|
||||||
|
yamlPath: file,
|
||||||
|
schemaPath: schema?.SchemaPath,
|
||||||
|
innerException: exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CollectReferenceUsages(
|
||||||
|
List<YamlConfigReferenceUsage> referenceUsages,
|
||||||
|
YamlConfigSchema? schema,
|
||||||
|
string file,
|
||||||
|
string yaml)
|
||||||
|
{
|
||||||
|
if (schema == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。
|
||||||
|
referenceUsages.AddRange(
|
||||||
|
YamlConfigSchemaValidator.ValidateAndCollectReferences(Name, schema, file, yaml));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TValue DeserializeValue(
|
||||||
|
IDeserializer deserializer,
|
||||||
|
string directoryPath,
|
||||||
|
string file,
|
||||||
|
YamlConfigSchema? schema,
|
||||||
|
string yaml,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var value = deserializer.Deserialize<TValue>(yaml);
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("YAML content was deserialized to null.");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// 同步反序列化阶段也要透传会话级取消,避免把停止加载误报为 YAML 解析失败。
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
throw ConfigLoadExceptionFactory.Create(
|
||||||
|
ConfigLoadFailureKind.DeserializationFailed,
|
||||||
|
Name,
|
||||||
|
$"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.",
|
||||||
|
configDirectoryPath: directoryPath,
|
||||||
|
yamlPath: file,
|
||||||
|
schemaPath: schema?.SchemaPath,
|
||||||
|
detail: $"Target CLR type: {typeof(TValue).FullName}.",
|
||||||
|
innerException: exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private YamlTableLoadResult BuildLoadResult(
|
||||||
|
string directoryPath,
|
||||||
|
YamlConfigSchema? schema,
|
||||||
|
List<TValue> values,
|
||||||
|
List<YamlConfigReferenceUsage> referenceUsages,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
var table = new InMemoryConfigTable<TKey, TValue>(values, _keySelector, _comparer);
|
var table = new InMemoryConfigTable<TKey, TValue>(values, _keySelector, _comparer);
|
||||||
return new YamlTableLoadResult(Name, table, referencedTableNames, referenceUsages);
|
return new YamlTableLoadResult(
|
||||||
|
Name,
|
||||||
|
table,
|
||||||
|
schema?.ReferencedTableNames ?? Array.Empty<string>(),
|
||||||
|
referenceUsages);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// 构建最终配置表时继续保留原始取消语义,避免热重载把提交前取消记录成构表失败。
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@ -630,6 +717,12 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static class CrossTableReferenceValidator
|
private static class CrossTableReferenceValidator
|
||||||
{
|
{
|
||||||
|
private delegate bool IntegerTryParseDelegate<T>(
|
||||||
|
string? value,
|
||||||
|
NumberStyles style,
|
||||||
|
IFormatProvider? provider,
|
||||||
|
out T result);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 使用本轮新加载结果与注册表中保留的旧表,一起验证跨表引用是否全部有效。
|
/// 使用本轮新加载结果与注册表中保留的旧表,一起验证跨表引用是否全部有效。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -754,59 +847,15 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
convertedKey = null;
|
convertedKey = null;
|
||||||
errorMessage = string.Empty;
|
errorMessage = string.Empty;
|
||||||
|
|
||||||
if (targetKeyType == typeof(int) &&
|
if (TryConvertIntegerKey<int>(rawValue, targetKeyType, typeof(int), int.TryParse, out convertedKey) ||
|
||||||
int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
TryConvertIntegerKey<long>(rawValue, targetKeyType, typeof(long), long.TryParse, out convertedKey) ||
|
||||||
|
TryConvertIntegerKey<short>(rawValue, targetKeyType, typeof(short), short.TryParse, out convertedKey) ||
|
||||||
|
TryConvertIntegerKey<byte>(rawValue, targetKeyType, typeof(byte), byte.TryParse, out convertedKey) ||
|
||||||
|
TryConvertIntegerKey<uint>(rawValue, targetKeyType, typeof(uint), uint.TryParse, out convertedKey) ||
|
||||||
|
TryConvertIntegerKey<ulong>(rawValue, targetKeyType, typeof(ulong), ulong.TryParse, out convertedKey) ||
|
||||||
|
TryConvertIntegerKey<ushort>(rawValue, targetKeyType, typeof(ushort), ushort.TryParse, out convertedKey) ||
|
||||||
|
TryConvertIntegerKey<sbyte>(rawValue, targetKeyType, typeof(sbyte), sbyte.TryParse, out convertedKey))
|
||||||
{
|
{
|
||||||
convertedKey = intValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetKeyType == typeof(long) &&
|
|
||||||
long.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue))
|
|
||||||
{
|
|
||||||
convertedKey = longValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetKeyType == typeof(short) &&
|
|
||||||
short.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var shortValue))
|
|
||||||
{
|
|
||||||
convertedKey = shortValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetKeyType == typeof(byte) &&
|
|
||||||
byte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var byteValue))
|
|
||||||
{
|
|
||||||
convertedKey = byteValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetKeyType == typeof(uint) &&
|
|
||||||
uint.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uintValue))
|
|
||||||
{
|
|
||||||
convertedKey = uintValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetKeyType == typeof(ulong) &&
|
|
||||||
ulong.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ulongValue))
|
|
||||||
{
|
|
||||||
convertedKey = ulongValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetKeyType == typeof(ushort) &&
|
|
||||||
ushort.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ushortValue))
|
|
||||||
{
|
|
||||||
convertedKey = ushortValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetKeyType == typeof(sbyte) &&
|
|
||||||
sbyte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sbyteValue))
|
|
||||||
{
|
|
||||||
convertedKey = sbyteValue;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -815,6 +864,25 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryConvertIntegerKey<T>(
|
||||||
|
string rawValue,
|
||||||
|
Type targetKeyType,
|
||||||
|
Type supportedType,
|
||||||
|
IntegerTryParseDelegate<T> tryParse,
|
||||||
|
out object? convertedKey)
|
||||||
|
where T : struct
|
||||||
|
{
|
||||||
|
convertedKey = null;
|
||||||
|
if (targetKeyType != supportedType ||
|
||||||
|
!tryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
convertedKey = parsedValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool ContainsKey(IConfigTable table, object key)
|
private static bool ContainsKey(IConfigTable table, object key)
|
||||||
{
|
{
|
||||||
var tableInterface = table.GetType()
|
var tableInterface = table.GetType()
|
||||||
@ -838,7 +906,13 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
new(StringComparer.Ordinal);
|
new(StringComparer.Ordinal);
|
||||||
|
|
||||||
private readonly IDeserializer _deserializer;
|
private readonly IDeserializer _deserializer;
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly Lock _gate = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
|
#endif
|
||||||
private readonly Action<string>? _onTableReloaded;
|
private readonly Action<string>? _onTableReloaded;
|
||||||
private readonly Action<string, Exception>? _onTableReloadFailed;
|
private readonly Action<string, Exception>? _onTableReloadFailed;
|
||||||
private readonly Dictionary<string, IYamlTableRegistration> _registrations = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, IYamlTableRegistration> _registrations = new(StringComparer.Ordinal);
|
||||||
@ -1121,7 +1195,7 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
|
|
||||||
foreach (var dependency in _dependenciesByTable)
|
foreach (var dependency in _dependenciesByTable)
|
||||||
{
|
{
|
||||||
if (!dependency.Value.Contains(currentTableName))
|
if (!ContainsDependency(dependency.Value, currentTableName))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1138,6 +1212,14 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ContainsDependency(
|
||||||
|
IReadOnlyCollection<string> dependencies,
|
||||||
|
string tableName)
|
||||||
|
{
|
||||||
|
return dependencies.Any(
|
||||||
|
dependency => string.Equals(dependency, tableName, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
private void InvokeReloaded(string tableName)
|
private void InvokeReloaded(string tableName)
|
||||||
{
|
{
|
||||||
if (_onTableReloaded == null)
|
if (_onTableReloaded == null)
|
||||||
|
|||||||
@ -52,7 +52,9 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
|
|||||||
var key = location.ToStorageKey();
|
var key = location.ToStorageKey();
|
||||||
|
|
||||||
// 检查存储中是否存在指定键的数据
|
// 检查存储中是否存在指定键的数据
|
||||||
T result = await Storage.ExistsAsync(key) ? await Storage.ReadAsync<T>(key) : new T();
|
T result = await Storage.ExistsAsync(key).ConfigureAwait(false)
|
||||||
|
? await Storage.ReadAsync<T>(key).ConfigureAwait(false)
|
||||||
|
: new T();
|
||||||
|
|
||||||
// 如果启用事件功能,则发送数据加载完成事件
|
// 如果启用事件功能,则发送数据加载完成事件
|
||||||
if (_options.EnableEvents)
|
if (_options.EnableEvents)
|
||||||
@ -70,7 +72,7 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
|
|||||||
public async Task SaveAsync<T>(IDataLocation location, T data)
|
public async Task SaveAsync<T>(IDataLocation location, T data)
|
||||||
where T : class, IData
|
where T : class, IData
|
||||||
{
|
{
|
||||||
await SaveCoreAsync(location, data, emitSavedEvent: true);
|
await SaveCoreAsync(location, data, emitSavedEvent: true).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -91,12 +93,12 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
|
|||||||
{
|
{
|
||||||
var key = location.ToStorageKey();
|
var key = location.ToStorageKey();
|
||||||
|
|
||||||
if (!await Storage.ExistsAsync(key))
|
if (!await Storage.ExistsAsync(key).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Storage.DeleteAsync(key);
|
await Storage.DeleteAsync(key).ConfigureAwait(false);
|
||||||
if (_options.EnableEvents)
|
if (_options.EnableEvents)
|
||||||
this.SendEvent(new DataDeletedEvent(location));
|
this.SendEvent(new DataDeletedEvent(location));
|
||||||
}
|
}
|
||||||
@ -113,7 +115,7 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
|
|||||||
// 但抑制逐项 DataSavedEvent,避免监听器对同一批次收到重复语义的事件。
|
// 但抑制逐项 DataSavedEvent,避免监听器对同一批次收到重复语义的事件。
|
||||||
foreach (var (location, data) in valueTuples)
|
foreach (var (location, data) in valueTuples)
|
||||||
{
|
{
|
||||||
await SaveCoreUntypedAsync(location, data, emitSavedEvent: false);
|
await SaveCoreUntypedAsync(location, data, emitSavedEvent: false).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_options.EnableEvents)
|
if (_options.EnableEvents)
|
||||||
@ -140,8 +142,8 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
|
|||||||
{
|
{
|
||||||
var key = location.ToStorageKey();
|
var key = location.ToStorageKey();
|
||||||
|
|
||||||
await BackupIfNeededAsync<T>(key);
|
await BackupIfNeededAsync<T>(key).ConfigureAwait(false);
|
||||||
await Storage.WriteAsync(key, data);
|
await Storage.WriteAsync(key, data).ConfigureAwait(false);
|
||||||
|
|
||||||
if (emitSavedEvent && _options.EnableEvents)
|
if (emitSavedEvent && _options.EnableEvents)
|
||||||
{
|
{
|
||||||
@ -156,14 +158,14 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
|
|||||||
private async Task BackupIfNeededAsync<T>(string key)
|
private async Task BackupIfNeededAsync<T>(string key)
|
||||||
where T : class, IData
|
where T : class, IData
|
||||||
{
|
{
|
||||||
if (!_options.AutoBackup || !await Storage.ExistsAsync(key))
|
if (!_options.AutoBackup || !await Storage.ExistsAsync(key).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var backupKey = $"{key}.backup";
|
var backupKey = $"{key}.backup";
|
||||||
var existing = await Storage.ReadAsync<T>(key);
|
var existing = await Storage.ReadAsync<T>(key).ConfigureAwait(false);
|
||||||
await Storage.WriteAsync(backupKey, existing);
|
await Storage.WriteAsync(backupKey, existing).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -33,7 +33,13 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
|||||||
{
|
{
|
||||||
private readonly SaveConfiguration _config;
|
private readonly SaveConfiguration _config;
|
||||||
private readonly Dictionary<int, ISaveMigration<TSaveData>> _migrations = new();
|
private readonly Dictionary<int, ISaveMigration<TSaveData>> _migrations = new();
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _migrationsLock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _migrationsLock = new();
|
private readonly object _migrationsLock = new();
|
||||||
|
#endif
|
||||||
private readonly IStorage _rootStorage;
|
private readonly IStorage _rootStorage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -99,7 +105,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
|||||||
public async Task<bool> ExistsAsync(int slot)
|
public async Task<bool> ExistsAsync(int slot)
|
||||||
{
|
{
|
||||||
var storage = GetSlotStorage(slot);
|
var storage = GetSlotStorage(slot);
|
||||||
return await storage.ExistsAsync(_config.SaveFileName);
|
return await storage.ExistsAsync(_config.SaveFileName).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -111,10 +117,10 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
|||||||
{
|
{
|
||||||
var storage = GetSlotStorage(slot);
|
var storage = GetSlotStorage(slot);
|
||||||
|
|
||||||
if (await storage.ExistsAsync(_config.SaveFileName))
|
if (await storage.ExistsAsync(_config.SaveFileName).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var loaded = await storage.ReadAsync<TSaveData>(_config.SaveFileName);
|
var loaded = await storage.ReadAsync<TSaveData>(_config.SaveFileName).ConfigureAwait(false);
|
||||||
return await MigrateIfNeededAsync(slot, storage, loaded);
|
return await MigrateIfNeededAsync(slot, storage, loaded).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new TSaveData();
|
return new TSaveData();
|
||||||
@ -130,11 +136,11 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
|||||||
var slotPath = $"{_config.SaveSlotPrefix}{slot}";
|
var slotPath = $"{_config.SaveSlotPrefix}{slot}";
|
||||||
|
|
||||||
// 确保槽位目录存在
|
// 确保槽位目录存在
|
||||||
if (!await _rootStorage.DirectoryExistsAsync(slotPath))
|
if (!await _rootStorage.DirectoryExistsAsync(slotPath).ConfigureAwait(false))
|
||||||
await _rootStorage.CreateDirectoryAsync(slotPath);
|
await _rootStorage.CreateDirectoryAsync(slotPath).ConfigureAwait(false);
|
||||||
|
|
||||||
var storage = GetSlotStorage(slot);
|
var storage = GetSlotStorage(slot);
|
||||||
await storage.WriteAsync(_config.SaveFileName, data);
|
await storage.WriteAsync(_config.SaveFileName, data).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -144,7 +150,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
|||||||
public async Task DeleteAsync(int slot)
|
public async Task DeleteAsync(int slot)
|
||||||
{
|
{
|
||||||
var storage = GetSlotStorage(slot);
|
var storage = GetSlotStorage(slot);
|
||||||
await storage.DeleteAsync(_config.SaveFileName);
|
await storage.DeleteAsync(_config.SaveFileName).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -154,7 +160,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
|||||||
public async Task<IReadOnlyList<int>> ListSlotsAsync()
|
public async Task<IReadOnlyList<int>> ListSlotsAsync()
|
||||||
{
|
{
|
||||||
// 列举所有槽位目录
|
// 列举所有槽位目录
|
||||||
var directories = await _rootStorage.ListDirectoriesAsync();
|
var directories = await _rootStorage.ListDirectoriesAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
var slots = new List<int>();
|
var slots = new List<int>();
|
||||||
|
|
||||||
@ -171,7 +177,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
|||||||
|
|
||||||
// 直接检查存档文件是否存在,避免重复创建 ScopedStorage
|
// 直接检查存档文件是否存在,避免重复创建 ScopedStorage
|
||||||
var saveFilePath = $"{dirName}/{_config.SaveFileName}";
|
var saveFilePath = $"{dirName}/{_config.SaveFileName}";
|
||||||
if (await _rootStorage.ExistsAsync(saveFilePath))
|
if (await _rootStorage.ExistsAsync(saveFilePath).ConfigureAwait(false))
|
||||||
slots.Add(slot);
|
slots.Add(slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,7 +252,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
|||||||
$"{typeof(TSaveData).Name} in slot {slot}",
|
$"{typeof(TSaveData).Name} in slot {slot}",
|
||||||
"save migration");
|
"save migration");
|
||||||
|
|
||||||
await storage.WriteAsync(_config.SaveFileName, migrated);
|
await storage.WriteAsync(_config.SaveFileName, migrated).ConfigureAwait(false);
|
||||||
return migrated;
|
return migrated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@ public class UnifiedSettingsDataRepository(
|
|||||||
{
|
{
|
||||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||||
private readonly DataRepositoryOptions _options = options ?? new DataRepositoryOptions();
|
private readonly DataRepositoryOptions _options = options ?? new DataRepositoryOptions();
|
||||||
private readonly Dictionary<string, Type> _typeRegistry = new();
|
private readonly Dictionary<string, Type> _typeRegistry = new(StringComparer.Ordinal);
|
||||||
private UnifiedSettingsFile? _file;
|
private UnifiedSettingsFile? _file;
|
||||||
private bool _loaded;
|
private bool _loaded;
|
||||||
private IRuntimeTypeSerializer? _serializer = serializer;
|
private IRuntimeTypeSerializer? _serializer = serializer;
|
||||||
@ -67,7 +67,7 @@ public class UnifiedSettingsDataRepository(
|
|||||||
public async Task<T> LoadAsync<T>(IDataLocation location)
|
public async Task<T> LoadAsync<T>(IDataLocation location)
|
||||||
where T : class, IData, new()
|
where T : class, IData, new()
|
||||||
{
|
{
|
||||||
await EnsureLoadedAsync();
|
await EnsureLoadedAsync().ConfigureAwait(false);
|
||||||
var key = location.Key;
|
var key = location.Key;
|
||||||
var result = _file!.Sections.TryGetValue(key, out var raw) ? Serializer.Deserialize<T>(raw) : new T();
|
var result = _file!.Sections.TryGetValue(key, out var raw) ? Serializer.Deserialize<T>(raw) : new T();
|
||||||
if (_options.EnableEvents)
|
if (_options.EnableEvents)
|
||||||
@ -85,8 +85,9 @@ public class UnifiedSettingsDataRepository(
|
|||||||
public async Task SaveAsync<T>(IDataLocation location, T data)
|
public async Task SaveAsync<T>(IDataLocation location, T data)
|
||||||
where T : class, IData
|
where T : class, IData
|
||||||
{
|
{
|
||||||
await EnsureLoadedAsync();
|
await EnsureLoadedAsync().ConfigureAwait(false);
|
||||||
await MutateAndPersistAsync(file => file.Sections[location.Key] = Serializer.Serialize(data));
|
await MutateAndPersistAsync(file => file.Sections[location.Key] = Serializer.Serialize(data))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (_options.EnableEvents)
|
if (_options.EnableEvents)
|
||||||
{
|
{
|
||||||
@ -101,7 +102,7 @@ public class UnifiedSettingsDataRepository(
|
|||||||
/// <returns>如果数据存在则返回true,否则返回false</returns>
|
/// <returns>如果数据存在则返回true,否则返回false</returns>
|
||||||
public async Task<bool> ExistsAsync(IDataLocation location)
|
public async Task<bool> ExistsAsync(IDataLocation location)
|
||||||
{
|
{
|
||||||
await EnsureLoadedAsync();
|
await EnsureLoadedAsync().ConfigureAwait(false);
|
||||||
return File.Sections.ContainsKey(location.Key);
|
return File.Sections.ContainsKey(location.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,10 +113,10 @@ public class UnifiedSettingsDataRepository(
|
|||||||
/// <returns>异步操作任务</returns>
|
/// <returns>异步操作任务</returns>
|
||||||
public async Task DeleteAsync(IDataLocation location)
|
public async Task DeleteAsync(IDataLocation location)
|
||||||
{
|
{
|
||||||
await EnsureLoadedAsync();
|
await EnsureLoadedAsync().ConfigureAwait(false);
|
||||||
var removed = false;
|
var removed = false;
|
||||||
|
|
||||||
await _lock.WaitAsync();
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var currentFile = File;
|
var currentFile = File;
|
||||||
@ -126,7 +127,7 @@ public class UnifiedSettingsDataRepository(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await WriteUnifiedFileCoreAsync(currentFile, nextFile);
|
await WriteUnifiedFileCoreAsync(currentFile, nextFile).ConfigureAwait(false);
|
||||||
_file = nextFile;
|
_file = nextFile;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -148,17 +149,18 @@ public class UnifiedSettingsDataRepository(
|
|||||||
public async Task SaveAllAsync(
|
public async Task SaveAllAsync(
|
||||||
IEnumerable<(IDataLocation location, IData data)> dataList)
|
IEnumerable<(IDataLocation location, IData data)> dataList)
|
||||||
{
|
{
|
||||||
await EnsureLoadedAsync();
|
await EnsureLoadedAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
var valueTuples = dataList.ToList();
|
var valueTuples = dataList.ToList();
|
||||||
|
|
||||||
await MutateAndPersistAsync(file =>
|
await MutateAndPersistAsync(file =>
|
||||||
{
|
|
||||||
foreach (var (location, data) in valueTuples)
|
|
||||||
{
|
{
|
||||||
file.Sections[location.Key] = Serializer.Serialize(data);
|
foreach (var (location, data) in valueTuples)
|
||||||
}
|
{
|
||||||
});
|
file.Sections[location.Key] = Serializer.Serialize(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (_options.EnableEvents)
|
if (_options.EnableEvents)
|
||||||
this.SendEvent(new DataBatchSavedEvent(valueTuples));
|
this.SendEvent(new DataBatchSavedEvent(valueTuples));
|
||||||
@ -170,9 +172,9 @@ public class UnifiedSettingsDataRepository(
|
|||||||
/// <returns>包含所有数据项的字典,键为数据位置键,值为数据对象</returns>
|
/// <returns>包含所有数据项的字典,键为数据位置键,值为数据对象</returns>
|
||||||
public async Task<IDictionary<string, IData>> LoadAllAsync()
|
public async Task<IDictionary<string, IData>> LoadAllAsync()
|
||||||
{
|
{
|
||||||
await EnsureLoadedAsync();
|
await EnsureLoadedAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
var result = new Dictionary<string, IData>();
|
var result = new Dictionary<string, IData>(StringComparer.Ordinal);
|
||||||
|
|
||||||
foreach (var (key, raw) in File.Sections)
|
foreach (var (key, raw) in File.Sections)
|
||||||
{
|
{
|
||||||
@ -216,15 +218,15 @@ public class UnifiedSettingsDataRepository(
|
|||||||
{
|
{
|
||||||
if (_loaded) return;
|
if (_loaded) return;
|
||||||
|
|
||||||
await _lock.WaitAsync();
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_loaded) return;
|
if (_loaded) return;
|
||||||
|
|
||||||
var key = UnifiedKey;
|
var key = UnifiedKey;
|
||||||
|
|
||||||
_file = await Storage.ExistsAsync(key)
|
_file = await Storage.ExistsAsync(key).ConfigureAwait(false)
|
||||||
? await Storage.ReadAsync<UnifiedSettingsFile>(key)
|
? await Storage.ReadAsync<UnifiedSettingsFile>(key).ConfigureAwait(false)
|
||||||
: new UnifiedSettingsFile { Version = 1 };
|
: new UnifiedSettingsFile { Version = 1 };
|
||||||
|
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
@ -241,7 +243,7 @@ public class UnifiedSettingsDataRepository(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task MutateAndPersistAsync(Action<UnifiedSettingsFile> mutation)
|
private async Task MutateAndPersistAsync(Action<UnifiedSettingsFile> mutation)
|
||||||
{
|
{
|
||||||
await _lock.WaitAsync();
|
await _lock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var currentFile = File;
|
var currentFile = File;
|
||||||
@ -250,7 +252,7 @@ public class UnifiedSettingsDataRepository(
|
|||||||
// 先在副本上计算“下一份已提交状态”,只有底层持久化成功后才交换缓存,
|
// 先在副本上计算“下一份已提交状态”,只有底层持久化成功后才交换缓存,
|
||||||
// 这样即使备份或写入失败,也不会把未提交修改留在内存快照里。
|
// 这样即使备份或写入失败,也不会把未提交修改留在内存快照里。
|
||||||
mutation(nextFile);
|
mutation(nextFile);
|
||||||
await WriteUnifiedFileCoreAsync(currentFile, nextFile);
|
await WriteUnifiedFileCoreAsync(currentFile, nextFile).ConfigureAwait(false);
|
||||||
_file = nextFile;
|
_file = nextFile;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -270,13 +272,13 @@ public class UnifiedSettingsDataRepository(
|
|||||||
/// <param name="nextFile">即将提交的新统一文件快照。</param>
|
/// <param name="nextFile">即将提交的新统一文件快照。</param>
|
||||||
private async Task WriteUnifiedFileCoreAsync(UnifiedSettingsFile currentFile, UnifiedSettingsFile nextFile)
|
private async Task WriteUnifiedFileCoreAsync(UnifiedSettingsFile currentFile, UnifiedSettingsFile nextFile)
|
||||||
{
|
{
|
||||||
if (_options.AutoBackup && await Storage.ExistsAsync(UnifiedKey))
|
if (_options.AutoBackup && await Storage.ExistsAsync(UnifiedKey).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var backupKey = $"{UnifiedKey}.backup";
|
var backupKey = $"{UnifiedKey}.backup";
|
||||||
await Storage.WriteAsync(backupKey, currentFile);
|
await Storage.WriteAsync(backupKey, currentFile).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Storage.WriteAsync(UnifiedKey, nextFile);
|
await Storage.WriteAsync(UnifiedKey, nextFile).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -31,8 +31,7 @@
|
|||||||
- 引擎适配包或项目内适配层
|
- 引擎适配包或项目内适配层
|
||||||
- 本包提供的是“引擎无关”的核心逻辑和基类。
|
- 本包提供的是“引擎无关”的核心逻辑和基类。
|
||||||
- 真正和 Godot、Unity、MonoGame 等引擎对象打交道的工厂、根节点、资源注册表,通常在相邻引擎包或游戏项目内实现。
|
- 真正和 Godot、Unity、MonoGame 等引擎对象打交道的工厂、根节点、资源注册表,通常在相邻引擎包或游戏项目内实现。
|
||||||
- 仓库内 `ai-libs/` 下的只读参考实现通常也是这样接入:配置文件 IO 由 `GFramework.Godot.Config` 适配,
|
- 典型项目里,配置文件 IO 会交给宿主适配层处理,UI / Scene 的 factory 与 root 则继续由项目自己提供。
|
||||||
UI / Scene factory 与 root 由项目自己提供。
|
|
||||||
|
|
||||||
## 子系统地图
|
## 子系统地图
|
||||||
|
|
||||||
@ -73,7 +72,7 @@
|
|||||||
- `SaveConfiguration`
|
- `SaveConfiguration`
|
||||||
- 槽位目录、文件名、前缀等约定
|
- 槽位目录、文件名、前缀等约定
|
||||||
|
|
||||||
`ai-libs/` 下已验证参考实现的常见接法:
|
常见接法:
|
||||||
|
|
||||||
- 设置持久化使用 `UnifiedSettingsDataRepository`
|
- 设置持久化使用 `UnifiedSettingsDataRepository`
|
||||||
- 存档使用 `SaveRepository<GameSaveData>`
|
- 存档使用 `SaveRepository<GameSaveData>`
|
||||||
@ -96,7 +95,7 @@
|
|||||||
- `Setting/Events/*`
|
- `Setting/Events/*`
|
||||||
- 设置初始化、应用、保存、重置相关事件
|
- 设置初始化、应用、保存、重置相关事件
|
||||||
|
|
||||||
`ai-libs/` 下已验证参考实现的常见接法:
|
常见接法:
|
||||||
|
|
||||||
- 在模型模块中创建 `SettingsModel<ISettingsDataRepository>`
|
- 在模型模块中创建 `SettingsModel<ISettingsDataRepository>`
|
||||||
- 注册多个 applicator
|
- 注册多个 applicator
|
||||||
@ -122,7 +121,6 @@
|
|||||||
对应文档:
|
对应文档:
|
||||||
|
|
||||||
- [存储系统](../docs/zh-CN/game/storage.md)
|
- [存储系统](../docs/zh-CN/game/storage.md)
|
||||||
- [Storage 子模块说明](./Storage/ReadMe.md)
|
|
||||||
|
|
||||||
### `Serializer/`
|
### `Serializer/`
|
||||||
|
|
||||||
@ -149,7 +147,7 @@
|
|||||||
- `Scene/Handler/*`、`UI/Handler/*`
|
- `Scene/Handler/*`、`UI/Handler/*`
|
||||||
- 默认转换处理器基类与日志处理器
|
- 默认转换处理器基类与日志处理器
|
||||||
|
|
||||||
`ai-libs/` 下已验证参考实现的常见接法:
|
常见接法:
|
||||||
|
|
||||||
- 项目自定义 `SceneRouter : SceneRouterBase`
|
- 项目自定义 `SceneRouter : SceneRouterBase`
|
||||||
- 项目自定义 `UiRouter : UiRouterBase`
|
- 项目自定义 `UiRouter : UiRouterBase`
|
||||||
@ -266,7 +264,7 @@ await settingsSystem.ApplyAll();
|
|||||||
await settingsSystem.SaveAll();
|
await settingsSystem.SaveAll();
|
||||||
```
|
```
|
||||||
|
|
||||||
`ai-libs/` 下的只读参考实现目前也是按这个思路接入,只是底层存储换成了 Godot 适配实现。
|
在 Godot 项目中也可以沿用同一思路,只是底层存储通常换成宿主侧适配实现。
|
||||||
|
|
||||||
### 3. 接入静态 YAML 配置
|
### 3. 接入静态 YAML 配置
|
||||||
|
|
||||||
@ -324,9 +322,9 @@ public sealed class MyUiRouter : UiRouterBase
|
|||||||
|
|
||||||
这类 router 适合作为你的项目层或引擎适配层代码,而不是直接修改本包。
|
这类 router 适合作为你的项目层或引擎适配层代码,而不是直接修改本包。
|
||||||
|
|
||||||
## `ai-libs/` 里的参考接入线索
|
## 典型项目分层方式
|
||||||
|
|
||||||
当前仓库内的只读参考实现,对本包的使用大致分成三层:
|
典型项目对本包的使用大致分成三层:
|
||||||
|
|
||||||
- 配置
|
- 配置
|
||||||
- 项目级配置宿主类型使用生成表元数据与 YAML loader 完成配置注册
|
- 项目级配置宿主类型使用生成表元数据与 YAML loader 完成配置注册
|
||||||
|
|||||||
@ -218,7 +218,7 @@ public abstract class RouterBase<TRoute, TContext> : AbstractSystem
|
|||||||
/// <returns>如果栈中包含指定路由返回 true,否则返回 false</returns>
|
/// <returns>如果栈中包含指定路由返回 true,否则返回 false</returns>
|
||||||
public bool Contains(string routeKey)
|
public bool Contains(string routeKey)
|
||||||
{
|
{
|
||||||
return Stack.Any(r => r.Key == routeKey);
|
return Stack.Any(r => string.Equals(r.Key, routeKey, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -237,7 +237,7 @@ public abstract class RouterBase<TRoute, TContext> : AbstractSystem
|
|||||||
/// <returns>如果栈顶是指定路由返回 true,否则返回 false</returns>
|
/// <returns>如果栈顶是指定路由返回 true,否则返回 false</returns>
|
||||||
public bool IsTop(string routeKey)
|
public bool IsTop(string routeKey)
|
||||||
{
|
{
|
||||||
return Stack.Count != 0 && Stack.Peek().Key.Equals(routeKey);
|
return Stack.Count != 0 && string.Equals(Stack.Peek().Key, routeKey, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
using System;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Extensions;
|
using GFramework.Core.Extensions;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
@ -82,7 +83,7 @@ public abstract class SceneRouterBase
|
|||||||
string sceneKey,
|
string sceneKey,
|
||||||
ISceneEnterParam? param = null)
|
ISceneEnterParam? param = null)
|
||||||
{
|
{
|
||||||
await _transitionLock.WaitAsync();
|
await _transitionLock.WaitAsync().ConfigureAwait(true);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IsTransitioning = true;
|
IsTransitioning = true;
|
||||||
@ -111,7 +112,7 @@ public abstract class SceneRouterBase
|
|||||||
/// <returns>如果场景在栈中返回true,否则返回false。</returns>
|
/// <returns>如果场景在栈中返回true,否则返回false。</returns>
|
||||||
public new bool Contains(string sceneKey)
|
public new bool Contains(string sceneKey)
|
||||||
{
|
{
|
||||||
return Stack.Any(s => s.Key == sceneKey);
|
return Stack.Any(s => string.Equals(s.Key, sceneKey, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -184,7 +185,7 @@ public abstract class SceneRouterBase
|
|||||||
string sceneKey,
|
string sceneKey,
|
||||||
ISceneEnterParam? param = null)
|
ISceneEnterParam? param = null)
|
||||||
{
|
{
|
||||||
await _transitionLock.WaitAsync();
|
await _transitionLock.WaitAsync().ConfigureAwait(true);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IsTransitioning = true;
|
IsTransitioning = true;
|
||||||
@ -220,7 +221,7 @@ public abstract class SceneRouterBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 守卫检查
|
// 守卫检查
|
||||||
if (!await ExecuteEnterGuardsAsync(sceneKey, param))
|
if (!await ExecuteEnterGuardsAsync(sceneKey, param).ConfigureAwait(true))
|
||||||
{
|
{
|
||||||
Log.Warn("Push blocked by guard: {0}", sceneKey);
|
Log.Warn("Push blocked by guard: {0}", sceneKey);
|
||||||
return;
|
return;
|
||||||
@ -233,20 +234,20 @@ public abstract class SceneRouterBase
|
|||||||
Root!.AddScene(scene);
|
Root!.AddScene(scene);
|
||||||
|
|
||||||
// 加载资源
|
// 加载资源
|
||||||
await scene.OnLoadAsync(param);
|
await scene.OnLoadAsync(param).ConfigureAwait(true);
|
||||||
|
|
||||||
// 暂停当前场景
|
// 暂停当前场景
|
||||||
if (Stack.Count > 0)
|
if (Stack.Count > 0)
|
||||||
{
|
{
|
||||||
var current = Stack.Peek();
|
var current = Stack.Peek();
|
||||||
await current.OnPauseAsync();
|
await current.OnPauseAsync().ConfigureAwait(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 压入栈
|
// 压入栈
|
||||||
Stack.Push(scene);
|
Stack.Push(scene);
|
||||||
|
|
||||||
// 进入场景
|
// 进入场景
|
||||||
await scene.OnEnterAsync();
|
await scene.OnEnterAsync().ConfigureAwait(true);
|
||||||
|
|
||||||
Log.Debug("Push Scene: {0}, stackCount={1}",
|
Log.Debug("Push Scene: {0}, stackCount={1}",
|
||||||
sceneKey, Stack.Count);
|
sceneKey, Stack.Count);
|
||||||
@ -262,7 +263,7 @@ public abstract class SceneRouterBase
|
|||||||
/// <returns>异步任务。</returns>
|
/// <returns>异步任务。</returns>
|
||||||
public async ValueTask PopAsync()
|
public async ValueTask PopAsync()
|
||||||
{
|
{
|
||||||
await _transitionLock.WaitAsync();
|
await _transitionLock.WaitAsync().ConfigureAwait(true);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IsTransitioning = true;
|
IsTransitioning = true;
|
||||||
@ -293,7 +294,7 @@ public abstract class SceneRouterBase
|
|||||||
var top = Stack.Peek();
|
var top = Stack.Peek();
|
||||||
|
|
||||||
// 守卫检查
|
// 守卫检查
|
||||||
if (!await ExecuteLeaveGuardsAsync(top.Key))
|
if (!await ExecuteLeaveGuardsAsync(top.Key).ConfigureAwait(true))
|
||||||
{
|
{
|
||||||
Log.Warn("Pop blocked by guard: {0}", top.Key);
|
Log.Warn("Pop blocked by guard: {0}", top.Key);
|
||||||
return;
|
return;
|
||||||
@ -302,10 +303,10 @@ public abstract class SceneRouterBase
|
|||||||
Stack.Pop();
|
Stack.Pop();
|
||||||
|
|
||||||
// 退出场景
|
// 退出场景
|
||||||
await top.OnExitAsync();
|
await top.OnExitAsync().ConfigureAwait(true);
|
||||||
|
|
||||||
// 卸载资源
|
// 卸载资源
|
||||||
await top.OnUnloadAsync();
|
await top.OnUnloadAsync().ConfigureAwait(true);
|
||||||
|
|
||||||
// 从场景树移除
|
// 从场景树移除
|
||||||
Root!.RemoveScene(top);
|
Root!.RemoveScene(top);
|
||||||
@ -314,7 +315,7 @@ public abstract class SceneRouterBase
|
|||||||
if (Stack.Count > 0)
|
if (Stack.Count > 0)
|
||||||
{
|
{
|
||||||
var next = Stack.Peek();
|
var next = Stack.Peek();
|
||||||
await next.OnResumeAsync();
|
await next.OnResumeAsync().ConfigureAwait(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug("Pop Scene, stackCount={0}", Stack.Count);
|
Log.Debug("Pop Scene, stackCount={0}", Stack.Count);
|
||||||
@ -330,7 +331,7 @@ public abstract class SceneRouterBase
|
|||||||
/// <returns>异步任务。</returns>
|
/// <returns>异步任务。</returns>
|
||||||
public async ValueTask ClearAsync()
|
public async ValueTask ClearAsync()
|
||||||
{
|
{
|
||||||
await _transitionLock.WaitAsync();
|
await _transitionLock.WaitAsync().ConfigureAwait(true);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IsTransitioning = true;
|
IsTransitioning = true;
|
||||||
|
|||||||
@ -29,7 +29,13 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
|
|
||||||
private readonly ConcurrentDictionary<Type, ISettingsData> _data = new();
|
private readonly ConcurrentDictionary<Type, ISettingsData> _data = new();
|
||||||
private readonly ConcurrentDictionary<Type, Dictionary<int, ISettingsMigration>> _migrationCache = new();
|
private readonly ConcurrentDictionary<Type, Dictionary<int, ISettingsMigration>> _migrationCache = new();
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
// net9.0 及以上目标使用专用 Lock,以满足分析器对专用同步原语的建议。
|
||||||
|
private readonly System.Threading.Lock _migrationMapLock = new();
|
||||||
|
#else
|
||||||
|
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
|
||||||
private readonly object _migrationMapLock = new();
|
private readonly object _migrationMapLock = new();
|
||||||
|
#endif
|
||||||
private readonly ConcurrentDictionary<(Type type, int from), ISettingsMigration> _migrations = new();
|
private readonly ConcurrentDictionary<(Type type, int from), ISettingsMigration> _migrations = new();
|
||||||
private volatile bool _initialized;
|
private volatile bool _initialized;
|
||||||
|
|
||||||
@ -169,7 +175,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
allData = await DataRepository.LoadAllAsync();
|
allData = await DataRepository.LoadAllAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -213,7 +219,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var location = LocationProvider.GetLocation(data.GetType());
|
var location = LocationProvider.GetLocation(data.GetType());
|
||||||
await DataRepository.SaveAsync(location, data);
|
await DataRepository.SaveAsync(location, data).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -231,7 +237,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
foreach (var applicator in _applicators)
|
foreach (var applicator in _applicators)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await applicator.Value.ApplyAsync();
|
await applicator.Value.ApplyAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -51,8 +51,8 @@ public static class UiInteractionProfiles
|
|||||||
{
|
{
|
||||||
return action switch
|
return action switch
|
||||||
{
|
{
|
||||||
UiInputAction.Cancel => (profile.CapturedActions & UiInputActionMask.Cancel) != 0,
|
UiInputAction.Cancel => (profile.CapturedActions & UiInputActionMask.Cancel) != UiInputActionMask.None,
|
||||||
UiInputAction.Confirm => (profile.CapturedActions & UiInputActionMask.Confirm) != 0,
|
UiInputAction.Confirm => (profile.CapturedActions & UiInputActionMask.Confirm) != UiInputActionMask.None,
|
||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -260,7 +260,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
/// <returns>如果栈顶是指定UI则返回true,否则返回false</returns>
|
/// <returns>如果栈顶是指定UI则返回true,否则返回false</returns>
|
||||||
public new bool IsTop(string uiKey)
|
public new bool IsTop(string uiKey)
|
||||||
{
|
{
|
||||||
return Stack.Count != 0 && Stack.Peek().Key.Equals(uiKey);
|
return Stack.Count != 0 && string.Equals(Stack.Peek().Key, uiKey, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -270,7 +270,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
/// <returns>如果栈中包含指定UI则返回true,否则返回false</returns>
|
/// <returns>如果栈中包含指定UI则返回true,否则返回false</returns>
|
||||||
public new bool Contains(string uiKey)
|
public new bool Contains(string uiKey)
|
||||||
{
|
{
|
||||||
return Stack.Any(p => p.Key.Equals(uiKey));
|
return Stack.Any(p => string.Equals(p.Key, uiKey, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -293,7 +293,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
public UiHandle Show(string uiKey, UiLayer layer, IUiPageEnterParam? param = null)
|
public UiHandle Show(string uiKey, UiLayer layer, IUiPageEnterParam? param = null)
|
||||||
{
|
{
|
||||||
if (layer == UiLayer.Page)
|
if (layer == UiLayer.Page)
|
||||||
throw new ArgumentException("Use Push() for Page layer");
|
throw new ArgumentException("Use Push() for Page layer", nameof(layer));
|
||||||
|
|
||||||
// 创建实例
|
// 创建实例
|
||||||
var page = _factory.Create(uiKey);
|
var page = _factory.Create(uiKey);
|
||||||
@ -311,7 +311,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
public UiHandle Show(IUiPageBehavior page, UiLayer layer)
|
public UiHandle Show(IUiPageBehavior page, UiLayer layer)
|
||||||
{
|
{
|
||||||
if (layer == UiLayer.Page)
|
if (layer == UiLayer.Page)
|
||||||
throw new ArgumentException("Use Push() for Page layer");
|
throw new ArgumentException("Use Push() for Page layer", nameof(layer));
|
||||||
|
|
||||||
return ShowInternal(page, layer, null);
|
return ShowInternal(page, layer, null);
|
||||||
}
|
}
|
||||||
@ -414,7 +414,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
return Array.Empty<UiHandle>();
|
return Array.Empty<UiHandle>();
|
||||||
|
|
||||||
return layerDict
|
return layerDict
|
||||||
.Where(kvp => kvp.Value.Key.Equals(uiKey))
|
.Where(kvp => string.Equals(kvp.Value.Key, uiKey, StringComparison.Ordinal))
|
||||||
.Select(kvp => new UiHandle(uiKey, kvp.Key, layer))
|
.Select(kvp => new UiHandle(uiKey, kvp.Key, layer))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@ -593,14 +593,18 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
|
|||||||
var handle = new UiHandle(page.Key, instanceId, layer);
|
var handle = new UiHandle(page.Key, instanceId, layer);
|
||||||
|
|
||||||
// 初始化层级字典
|
// 初始化层级字典
|
||||||
if (!_layers.ContainsKey(layer))
|
if (!_layers.TryGetValue(layer, out var layerDict))
|
||||||
_layers[layer] = new Dictionary<string, IUiPageBehavior>();
|
{
|
||||||
|
layerDict = new Dictionary<string, IUiPageBehavior>(StringComparer.Ordinal);
|
||||||
|
_layers[layer] = layerDict;
|
||||||
|
}
|
||||||
|
|
||||||
// 设置句柄
|
// 设置句柄
|
||||||
page.Handle = handle;
|
page.Handle = handle;
|
||||||
var layerDict = _layers[layer];
|
|
||||||
|
|
||||||
// 检查重入性
|
// 检查重入性
|
||||||
if (!page.IsReentrant && layerDict.Values.Any(p => p.Key == page.Key))
|
if (!page.IsReentrant &&
|
||||||
|
layerDict.Values.Any(p => string.Equals(p.Key, page.Key, StringComparison.Ordinal)))
|
||||||
{
|
{
|
||||||
Log.Warn("UI {0} is not reentrant but already exists in layer {1}", page.Key, layer);
|
Log.Warn("UI {0} is not reentrant but already exists in layer {1}", page.Key, layer);
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
|
|||||||
@ -119,23 +119,19 @@ Godot 上。
|
|||||||
|
|
||||||
### 4. 按需接入配置、存储和设置
|
### 4. 按需接入配置、存储和设置
|
||||||
|
|
||||||
当项目已经使用 `Game` family 的配置、存储、设置契约时,再补 Godot 侧实现:
|
当项目已经使用 `Game` 模块的配置、存储、设置契约时,再补 Godot 侧实现:
|
||||||
|
|
||||||
- 配置:`GodotYamlConfigLoader`
|
- 配置:`GodotYamlConfigLoader`
|
||||||
- 存储:`GodotFileStorage`
|
- 存储:`GodotFileStorage`
|
||||||
- 设置:`GodotAudioSettings`、`GodotGraphicsSettings`、`GodotLocalizationSettings`
|
- 设置:`GodotAudioSettings`、`GodotGraphicsSettings`、`GodotLocalizationSettings`
|
||||||
|
|
||||||
不要把这些宿主实现误写成 `Game` family 的默认行为。
|
不要把这些宿主实现误写成 `Game` 模块的默认行为。
|
||||||
|
|
||||||
## `ai-libs/` 里的参考接入线索
|
## 典型接入方式
|
||||||
|
|
||||||
`ai-libs/CoreGrid` 仍是当前最直接的消费者证据来源:
|
|
||||||
|
|
||||||
- 架构侧保持普通模块注册,再按需挂接 Godot 宿主
|
- 架构侧保持普通模块注册,再按需挂接 Godot 宿主
|
||||||
- `project.godot` 元数据与节点样板交给 `GFramework.Godot.SourceGenerators`
|
- `project.godot` 元数据与节点样板交给 `GFramework.Godot.SourceGenerators`
|
||||||
- Scene / UI 继续沿用 `Game` family 的 router 语义
|
- Scene / UI 继续沿用 `Game` 模块的路由语义
|
||||||
|
|
||||||
当 `ai-libs/` 与源码或测试冲突时,应以当前源码与测试为准。
|
|
||||||
|
|
||||||
## 文档入口
|
## 文档入口
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
# Analyzer Warning Reduction 跟踪归档(RP074-RP078)
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
- 归档 `RP074` 到 `RP078` 期间从 active todo 中迁出的批次明细。
|
||||||
|
- 保留当前波次的已完成 slice 摘要、验证收口与延后候选,供后续恢复时回溯。
|
||||||
|
|
||||||
|
## 已完成批次摘要
|
||||||
|
|
||||||
|
- 第一轮并行 warning 清理:
|
||||||
|
- `GFramework.Core` 事件 / 状态 / 属性 / 协程统计中的 `MA0158` 专用锁迁移
|
||||||
|
- `GFramework.Game/Data` 中 `DataRepository`、`UnifiedSettingsDataRepository`、`SaveRepository` 的 `ConfigureAwait` / 比较器 / 专用锁修正
|
||||||
|
- `GFramework.Game/Scene/SceneRouterBase.cs` 与 `GFramework.Game/UI/UiRouterBase.cs` 中的显式上下文 / 参数名 / 比较器修正
|
||||||
|
- 收口提交:`fb0a55f` `fix(analyzer): 收口首轮并行警告清理`
|
||||||
|
- 第三轮 `Core.Tests` 低风险 slice:
|
||||||
|
- `GFramework.Core.Tests/Concurrency/AsyncKeyLockManagerTests.cs` 的 `MA0004`
|
||||||
|
- `GFramework.Core.Tests/Pause/PauseStackManagerTests.cs` 的 `MA0158`
|
||||||
|
- `GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs` 的 `MA0015`
|
||||||
|
- `GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs` 的 `MA0004`
|
||||||
|
|
||||||
|
## 批次验证快照
|
||||||
|
|
||||||
|
- `dotnet clean`
|
||||||
|
- 结果:提权直接执行成功,确认为当前权威 clean 基线
|
||||||
|
- `dotnet build`
|
||||||
|
- 结果:提权直接构建成功;warning 从 `639` 降到 `397`
|
||||||
|
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||||
|
- 结果:提权直接构建成功;`0 Warning(s)`、`0 Error(s)`
|
||||||
|
|
||||||
|
## 延后候选
|
||||||
|
|
||||||
|
- `GFramework.Game/Config/YamlConfigLoader.cs` 的 `MA0158`
|
||||||
|
- 原因:单点可修,但文件同时承载其他高耦合 warning,不适合在当前低风险批次顺手推进
|
||||||
|
- 测试项目中的 `MA0048` 文件名拆分波次
|
||||||
|
- 原因:会显著增加 changed-file 数,更适合另开后续波次
|
||||||
|
|
||||||
|
## 关联资料
|
||||||
|
|
||||||
|
- 详细执行过程见 [analyzer-warning-reduction-history-rp073-rp078.md](../traces/analyzer-warning-reduction-history-rp073-rp078.md)。
|
||||||
@ -0,0 +1,176 @@
|
|||||||
|
# Analyzer Warning Reduction 追踪归档(RP073-RP078)
|
||||||
|
|
||||||
|
## 2026-04-27 — RP-078
|
||||||
|
|
||||||
|
### 阶段:完成第三轮 Core.Tests 低风险 slice 并在 30 files 处收口
|
||||||
|
|
||||||
|
- 触发背景:
|
||||||
|
- 第二轮结束后,`GFramework.Game` 低风险单文件 warning 已基本耗尽,继续推进更适合转向测试项目
|
||||||
|
- 第三轮选择的 `Core.Tests` slice 仍保持单文件、低耦合,且不会明显放大 branch diff
|
||||||
|
- 已接受的 delegated scope 与结果:
|
||||||
|
- worker-A:`GFramework.Core.Tests/Concurrency/AsyncKeyLockManagerTests.cs`
|
||||||
|
- 结果:与 `PauseStackManagerTests.cs` 一并落在提交 `650618b`,修复该文件的 `MA0004`
|
||||||
|
- worker-B:`GFramework.Core.Tests/Pause/PauseStackManagerTests.cs`
|
||||||
|
- 结果:与 `AsyncKeyLockManagerTests.cs` 一并落在提交 `650618b`,修复该文件的 `MA0158`
|
||||||
|
- worker-C:`GFramework.Core.Tests/Extensions/AsyncExtensionsTests.cs`、`GFramework.Core.Tests/Architectures/ArchitectureModulesBehaviorTests.cs`
|
||||||
|
- 结果:提交 `e19e60e`,修复 `MA0015` / `MA0004`
|
||||||
|
- 主线程验证里程碑:
|
||||||
|
- 提权 `dotnet clean`
|
||||||
|
- 结果:成功
|
||||||
|
- 提权 `dotnet build`
|
||||||
|
- 结果:成功;warning 从上一轮的 `405` 降到 `397`
|
||||||
|
- 提权 `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||||
|
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||||
|
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
|
||||||
|
- 结果:`30`
|
||||||
|
- `git diff --numstat refs/remotes/origin/main...HEAD`
|
||||||
|
- 结果:`642` changed lines
|
||||||
|
- 当前结论:
|
||||||
|
- 当前分支在 `30 / 50` files 时仍保持可审阅性,且已经连续三轮拿到了实质 warning 降幅
|
||||||
|
- 继续推进的剩余候选主要是 `YamlConfig*` 高耦合热点与 `MA0048` 批量拆分,不再符合本轮的低风险边界
|
||||||
|
- 默认建议在这里收口当前波次,把下一波次留给更明确的热点专项
|
||||||
|
|
||||||
|
## 2026-04-27 — RP-077
|
||||||
|
|
||||||
|
### 阶段:完成第二轮 Game 侧低风险 slice 验证并转向测试项目候选
|
||||||
|
|
||||||
|
- 触发背景:
|
||||||
|
- 第二轮 worker 已分别完成 `SettingsModel.cs`、`RouterBase.cs`+`UiInteractionProfiles.cs`、`GameConfigBootstrap.cs`
|
||||||
|
- 主线程在复验时发现 `SettingsModel.cs` 与 `GameConfigBootstrap.cs` 又各暴露一个 touched-file `MA0158`,已在主线程补齐
|
||||||
|
- 已接受的 delegated scope 与结果:
|
||||||
|
- worker-A:`GFramework.Game/Setting/SettingsModel.cs`
|
||||||
|
- 结果:提交 `c106e53`,修复 `MA0004`;主线程随后补齐同文件 `MA0158`
|
||||||
|
- worker-B:`GFramework.Game/Routing/RouterBase.cs`、`GFramework.Game/UI/UiInteractionProfiles.cs`
|
||||||
|
- 结果:提交 `9deafac`,修复 `MA0006` / `MA0099`
|
||||||
|
- worker-C:`GFramework.Game/Config/GameConfigBootstrap.cs`
|
||||||
|
- 结果:提交 `9ce634e`,拆分热重载启动流程以修复 `MA0051`;主线程随后补齐同文件 `MA0158`
|
||||||
|
- explorer:重新审视 `GFramework.Game` 排除热点后的剩余候选
|
||||||
|
- 结果:确认 `Game` 侧低风险单文件 warning 基本耗尽,继续推进应转向其他项目
|
||||||
|
- 主线程验证里程碑:
|
||||||
|
- 提权 `dotnet clean`
|
||||||
|
- 结果:成功
|
||||||
|
- 提权 `dotnet build`
|
||||||
|
- 结果:成功;warning 从上一轮的 `430` 继续降到 `405`
|
||||||
|
- 提权 `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||||
|
- 结果:成功;warning 从上一轮的 `147` 降到 `122`
|
||||||
|
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
|
||||||
|
- 结果:`26`
|
||||||
|
- `git diff --numstat refs/remotes/origin/main...HEAD`
|
||||||
|
- 结果:`483` changed lines
|
||||||
|
- 当前结论:
|
||||||
|
- 第二轮 Game 侧 warning 清理已完成验证,且 warning 数继续实质下降
|
||||||
|
- 当前分支距离 `$gframework-batch-boot 50` 仍有空间,但继续推进不应再硬碰 `YamlConfigSchemaValidator*` / `YamlConfigLoader.cs`
|
||||||
|
- 若继续下一轮,优先切向 `Core.Tests` 等测试项目里的单文件 `MA0004` / `MA0015` / `MA0158`
|
||||||
|
|
||||||
|
## 2026-04-27 — RP-076
|
||||||
|
|
||||||
|
### 阶段:首轮收口提交后进入第二轮低风险 Game warning slice
|
||||||
|
|
||||||
|
- 触发背景:
|
||||||
|
- 首轮并行清理已经以 `fb0a55f` 收口,当前分支相对 `origin/main` 的累计改动文件数来到 `22 / 50`
|
||||||
|
- 用户要求继续采用“先拿构建 warning,再分批交给 subagent”模式,因此当前仍有继续推进的 branch 预算
|
||||||
|
- 主线程当前真值:
|
||||||
|
- 当前基线:`refs/remotes/origin/main` = `617e0bf`
|
||||||
|
- 当前 `HEAD` stop metric:
|
||||||
|
- files:`22`
|
||||||
|
- changed lines:`378`
|
||||||
|
- 最近权威验证仍为:
|
||||||
|
- `dotnet build`:`430 Warning(s)`、`0 Error(s)`
|
||||||
|
- `dotnet build GFramework.sln -c Release`:`147 Warning(s)`、`0 Error(s)`
|
||||||
|
- 本轮拟下发的 delegated scope:
|
||||||
|
- worker-A:`GFramework.Game/Setting/SettingsModel.cs`
|
||||||
|
- 目标:修复 `MA0004`,仅在不改变设置模型生命周期语义的前提下补全 `ConfigureAwait(false)`
|
||||||
|
- worker-B:`GFramework.Game/Routing/RouterBase.cs` 与 `GFramework.Game/UI/UiInteractionProfiles.cs`
|
||||||
|
- 目标:修复 `MA0006` / `MA0099`,保持现有路由比较语义与 UI 动作位掩码语义不变
|
||||||
|
- worker-C:`GFramework.Game/Config/GameConfigBootstrap.cs`
|
||||||
|
- 目标:评估并尽量修复 `MA0051`;若单文件安全提取不可低风险完成,应明确放弃并说明阻塞点
|
||||||
|
- 当前结论:
|
||||||
|
- 第二轮继续严格限制在低风险单文件 slice,避免直接进入 `YamlConfigSchemaValidator*` 与 `YamlConfigLoader.cs` 这种高耦合热点
|
||||||
|
- 本轮完成后应重新评估 branch diff 是否仍适合继续在同一分支上批量推进
|
||||||
|
|
||||||
|
## 2026-04-27 — RP-075
|
||||||
|
|
||||||
|
### 阶段:完成 `$gframework-batch-boot 50` 第一轮并行 warning 清理集成
|
||||||
|
|
||||||
|
- 触发背景:
|
||||||
|
- 用户要求先以权威构建输出建立 warning 基线,再把低风险 warning family 按文件边界拆给不同 subagent 并行清理
|
||||||
|
- 当前批次已完成首轮 worker 集成,但第二组锁迁移、主线程补修与 `ai-plan` 同步仍在工作树,需先收口提交再进入下一轮
|
||||||
|
- 已接受的 delegated scope 与结果:
|
||||||
|
- worker-1:`GFramework.Core` 事件 / 状态 / 属性 / 协程统计中的 `MA0158`
|
||||||
|
- 结果:已提交 `8f2d959`,采用 `#if NET9_0_OR_GREATER` + `System.Threading.Lock` / `object` 双分支兼容模式
|
||||||
|
- worker-2:`GFramework.Core` / `GFramework.Cqrs` 资源、日志、配置缓存中的 `MA0158`
|
||||||
|
- 结果:改动已集成到工作树,待主线程与本轮 `ai-plan` 一并提交
|
||||||
|
- worker-3:`GFramework.Game/Data` 与 `SceneRouterBase.cs`
|
||||||
|
- 结果:已提交 `e3eec54`,主线程随后补修 `SceneRouterBase.Contains` 与 `SaveRepository._migrationsLock` 的 touched-file 残留 warning
|
||||||
|
- worker-4:`GFramework.Game/UI/UiRouterBase.cs`
|
||||||
|
- 结果:已提交 `7e13752`
|
||||||
|
- 主线程验证里程碑:
|
||||||
|
- 提权 `dotnet clean`
|
||||||
|
- 结果:成功
|
||||||
|
- 提权 `dotnet build`
|
||||||
|
- 结果:成功;warning 从本轮批次建立时的 `639` 降到 `430`
|
||||||
|
- 提权 `dotnet build GFramework.sln -c Release`
|
||||||
|
- 结果:成功;`147 Warning(s)`、`0 Error(s)`
|
||||||
|
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
|
||||||
|
- 结果:`12`
|
||||||
|
- `git diff --numstat refs/remotes/origin/main...HEAD`
|
||||||
|
- 结果:`192` changed lines
|
||||||
|
- 当前结论:
|
||||||
|
- 第一轮并行 warning 清理已经完成验证,且 warning 总量出现明显下降,可以继续按 batch 模式推进
|
||||||
|
- 当前 stop-condition 仍远低于 `$gframework-batch-boot 50`;但在派发下一轮之前,应该先提交当前工作树里的第二组锁迁移与恢复文档同步
|
||||||
|
- 下一轮优先目标保持“低风险、单文件、避免高耦合热点”,候选包括 `SettingsModel.cs`、`RouterBase.cs`、`UiInteractionProfiles.cs`
|
||||||
|
|
||||||
|
## 2026-04-27 — RP-074
|
||||||
|
|
||||||
|
### 阶段:按 `$gframework-batch-boot 50` 建立并行 warning 清理批次
|
||||||
|
|
||||||
|
- 触发背景:
|
||||||
|
- 用户明确要求在拿到构建 warning 后分批指派给不同 subagent,以控制主线程上下文长度并提高 warning 清理效率
|
||||||
|
- 当前 worktree 映射到 `analyzer-warning-reduction` 主题,且该任务符合 batch candidate 条件:重复、可切片、可按文件边界独立验证
|
||||||
|
- 基线与停止条件:
|
||||||
|
- 当前基线采用 `refs/remotes/origin/main`
|
||||||
|
- `origin/main` 与 `HEAD` 当前同为 `617e0bf`(`2026-04-26T12:17:15+08:00`)
|
||||||
|
- 主 stop condition 为 branch diff files 接近 `50`;当前为 `0 / 50`
|
||||||
|
- 主线程实施:
|
||||||
|
- 先读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md` 以及当前 topic 的 active todo/trace,确认批处理流程与 topic 上下文
|
||||||
|
- 先在沙箱内执行仓库根 `dotnet clean` / `dotnet build`;其中 `dotnet clean` 因缺失 Windows fallback package folder 失败,判定为环境噪音
|
||||||
|
- 按仓库规则提权重跑直接命令,确认权威基线为 `dotnet clean` 成功、`dotnet build` 成功且 `639 Warning(s)`、`0 Error(s)`
|
||||||
|
- 基于当前 warning 输出,预划分以下互不重叠的 subagent ownership:
|
||||||
|
- `GFramework.Core` / `GFramework.Cqrs` 的 `MA0158` 专用锁迁移
|
||||||
|
- `GFramework.Game/Data` 的 `MA0004` 与局部 `MA0002`
|
||||||
|
- `GFramework.Game/Scene/SceneRouterBase.cs`、`GFramework.Game/UI/UiRouterBase.cs` 的显式上下文 / 参数名 / 比较器修正
|
||||||
|
- 验证里程碑:
|
||||||
|
- `dotnet clean`
|
||||||
|
- 结果:提权后成功;作为本轮 clean 真值
|
||||||
|
- `dotnet build`
|
||||||
|
- 结果:提权后成功;`639 Warning(s)`、`0 Error(s)`
|
||||||
|
- `git diff --name-only refs/remotes/origin/main...HEAD | wc -l`
|
||||||
|
- 结果:`0`
|
||||||
|
- `git diff --numstat refs/remotes/origin/main...HEAD`
|
||||||
|
- 结果:空输出
|
||||||
|
- 当前结论:
|
||||||
|
- 本轮已经完成 batch boot 所需的权威警告基线建立,可以安全进入并行 worker 阶段
|
||||||
|
- 当前优先级应继续保持在低风险、少文件、可独立验证的 warning family 上,不直接扩展到 `YamlConfigSchemaValidator` 这类高耦合热点
|
||||||
|
- 下一步默认由主线程下发 disjoint worker 任务并在集成后重新计算 branch diff 与 warning 结果
|
||||||
|
|
||||||
|
## 2026-04-26 — RP-073
|
||||||
|
|
||||||
|
### 阶段:脱敏 analyzer-warning-reduction 文档中的绝对路径记录
|
||||||
|
|
||||||
|
- 触发背景:
|
||||||
|
- 用户再次显式要求执行 `$gframework-pr-review`,当前分支仍对应 PR `#291`
|
||||||
|
- 最新抓取结果确认 latest-head 还剩 `2` 条 open review thread,分别指向 active todo 与 archive trace 中记录的绝对路径
|
||||||
|
- active trace 当前也保留了同类 `/tmp` 路径记录;虽然这次 review 没直接点名,但继续保留会留下同一类治理缺口
|
||||||
|
- 主线程实施:
|
||||||
|
- 将 active todo 与 active trace 中的 PR review 输出路径改写为 `--json-output <current-pr-review-json>`
|
||||||
|
- 将 [analyzer-warning-reduction-history-rp062-rp071.md](analyzer-warning-reduction-history-rp062-rp071.md) 里的临时 `dotnet` home、PR review 输出路径和失效 Windows fallback package folder 改写为仓库安全占位符
|
||||||
|
- 同步刷新 active todo 中的 review 真值,把当前恢复点更新到 `RP-073`
|
||||||
|
- 验证里程碑:
|
||||||
|
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
|
||||||
|
- 结果:成功;确认 PR `#291` latest-head open review thread 为 `2`,两者都指向 `ai-plan` 文档中的绝对路径记录
|
||||||
|
- `dotnet build`
|
||||||
|
- 结果:成功;`639 Warning(s)`、`0 Error(s)`;与当前权威仓库根基线一致
|
||||||
|
- 当前结论:
|
||||||
|
- 本轮只吸收当前仍成立的 PR review 文档项,不扩展到新的 warning 清理切片
|
||||||
|
- 当前仓库根 warning 权威基线仍保持 `639 Warning(s)`、`0 Error(s)`;本轮目标是让 analyzer-warning-reduction 主题下当前入口不再记录绝对路径
|
||||||
|
- 下一轮默认先推送本轮同步并重新执行 `$gframework-pr-review`,确认 PR `#291` 的 open thread 是否已自动收口
|
||||||
@ -6,46 +6,54 @@
|
|||||||
|
|
||||||
## 当前恢复点
|
## 当前恢复点
|
||||||
|
|
||||||
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-073`
|
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-084`
|
||||||
- 当前阶段:`Phase 73`
|
- 当前阶段:`Phase 84`
|
||||||
- 当前焦点:
|
- 当前焦点:
|
||||||
- `2026-04-26` 主线程再次按 `$gframework-pr-review` 复核当前分支 PR `#291`,确认 latest-head 仍剩 `2` 条 open review thread,均指向 `ai-plan` 文档中的绝对路径记录
|
- `2026-04-27` 已完成 PR `#297` 的 CodeRabbit follow-up,修复 `YamlConfigLoader` 的取消语义与 `IntegerTryParseDelegate` 可空性问题
|
||||||
- 当前批次同步 active todo/trace 与相关 archive trace:把 PR review 输出路径、临时 `dotnet` home 和失效 Windows fallback package folder 改写为仓库安全占位符
|
- 已补齐 `GFramework.Core.Tests/Ioc` 与 `GFramework.Core.Tests/Query` 中 review 指向的 XML 文档缺口,并让 `IPrioritizedService` 复用 `IMixedService.Name` 契约
|
||||||
- `dotnet clean` + `dotnet build` 的直接仓库根基线仍为 `639 Warning(s)`、`0 Error(s)`,因此本轮属于文档真值收口,而不是新的 warning 清理批次
|
- 已新增 `YamlConfigLoaderTests` 回归测试,锁定“取消时保留 `OperationCanceledException`”这一行为
|
||||||
|
- 当前分支的下一波 warning reduction 仍建议回到 `ArchitectureContextTests.cs`、`AsyncQueryExecutorTests.cs` 或 `YamlConfigSchemaValidator*` 的后续 slice
|
||||||
|
|
||||||
## 当前活跃事实
|
## 当前活跃事实
|
||||||
|
|
||||||
- 当前 `origin/main` 基线提交为 `4ad880c`(`2026-04-25T14:35:38+08:00`)。
|
- 当前 `origin/main` 基线提交为 `b6a9fef`(`2026-04-27T10:53:34+08:00`)。
|
||||||
- 提权后的直接仓库根验证当前确认为:
|
- 当前直接验证结果:
|
||||||
- `dotnet clean`
|
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||||
- 结果:成功;此前沙箱内 “Build FAILED but 0 errors” 的 clean 结果不是仓库真值
|
- 最新结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||||
- `dotnet build`
|
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||||
- 最新结果:成功;`639 Warning(s)`、`0 Error(s)`
|
- 最新结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||||
- 当前分支低风险批次文件:
|
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests.ReadYamlAsync_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested"`
|
||||||
- `ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md`
|
- 最新结果:成功;`1` 通过、`0` 失败
|
||||||
- `ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md`
|
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests.GetAllByPriority_Should_Sort_By_Priority_Ascending"`
|
||||||
- `ai-plan/public/analyzer-warning-reduction/archive/traces/analyzer-warning-reduction-history-rp062-rp071.md`
|
- 最新结果:成功;`1` 通过、`0` 失败
|
||||||
- 当前批次验证结果:
|
- `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigLoader.cs GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs GFramework.Core.Tests/Ioc/IMixedService.cs GFramework.Core.Tests/Ioc/IPrioritizedService.cs GFramework.Core.Tests/Ioc/PrioritizedService.cs GFramework.Core.Tests/Query/TestAsyncQueryWithExceptionV4.cs`
|
||||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
|
- 最新结果:成功;本次 PR follow-up 改动文件无需额外格式化
|
||||||
- 最新主线程结果:成功;确认 PR `#291` latest-head open review thread 为 `2`,两者都指向 `ai-plan` 文档中的绝对路径记录
|
- 当前批次摘要:
|
||||||
- `dotnet build`
|
- 本轮完成 PR `#297` 最新 head review 中仍然有效的 `3` 个 open threads 修复:`YamlConfigLoader` 取消语义、`IMixedService.Name` XML 文档、`IPrioritizedService` 相关契约整理
|
||||||
- 最新主线程结果:成功;`639 Warning(s)`、`0 Error(s)`;与当前权威仓库根基线一致
|
- 本轮同时吸收 CodeRabbit folded nitpick 中仍然成立的 `2` 个点:`IntegerTryParseDelegate` 可空性对齐、`TestAsyncQueryWithExceptionV4.OnDoAsync` 的 `<returns>` 文档
|
||||||
|
- 本轮新增一条精确回归测试,确保底层 YAML 文件读取在取消时继续抛出 `OperationCanceledException` 系列异常,而不是包装成 `ConfigLoadException`
|
||||||
|
- 当前建议保留到下一波次的候选:
|
||||||
|
- `GFramework.Core.Tests/Architectures/ArchitectureContextTests.cs` 的 `7` 个 `MA0048`
|
||||||
|
- `GFramework.Core.Tests/Query/AsyncQueryExecutorTests.cs` 的 `7` 个 `MA0048`
|
||||||
|
- `GFramework.Game/Config/YamlConfigSchemaValidator.cs` 与 `YamlConfigSchemaValidator.ObjectKeywords.cs` 的高耦合 warning 热点
|
||||||
|
|
||||||
## 当前风险
|
## 当前风险
|
||||||
|
|
||||||
- `GFramework.Core`、`GFramework.Game`、`GFramework.Core.Tests`、`GFramework.Cqrs.Tests` 仍有较大 warning 基线。
|
- `GFramework.Cqrs.Tests/Mediator/*` 仍有 `47` / `44` / `34` 个唯一 warning 位点,属于高 changed-file 风险的 `MA0048` 大波次。
|
||||||
- 缓解措施:后续批次继续优先挑低风险、少文件、可独立验证的测试与局部逻辑切片。
|
- 缓解措施:优先继续处理 `6-7` 个 warning 的小文件切片,避免一次性推高文件数。
|
||||||
- 当前 review 相关真值要等新 head 推送后才能在 GitHub UI 中自动收口。
|
- `YamlConfigSchemaValidator*` 仍然聚集多类高耦合 warning。
|
||||||
- 缓解措施:本轮提交后立即重新执行 `$gframework-pr-review`,确认 PR `#291` 的 latest-head thread 与 nitpick 是否消失。
|
- 缓解措施:继续把它们留在独立波次,不与测试项目的低风险拆分混提。
|
||||||
|
|
||||||
## 活跃文档
|
## 活跃文档
|
||||||
|
|
||||||
- 当前轮次归档:
|
- 当前轮次归档:
|
||||||
|
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
|
||||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||||
- 历史跟踪归档:
|
- 历史跟踪归档:
|
||||||
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
||||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||||
- 历史 trace 归档:
|
- 历史 trace 归档:
|
||||||
|
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
|
||||||
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
||||||
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
||||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||||
@ -53,11 +61,12 @@
|
|||||||
|
|
||||||
## 验证说明
|
## 验证说明
|
||||||
|
|
||||||
- 权威验证结果统一维护在“当前活跃事实”和“当前批次验证结果”。
|
- 权威验证结果统一维护在“当前活跃事实”。
|
||||||
- 后续若刷新构建或 PR review 真值,只更新上述权威区块,不在本节重复抄录。
|
- `GFramework.Core.Tests` 项目级 Release 构建已在本轮清零,但仓库根 non-incremental 构建仍保留大量既有 warning。
|
||||||
|
- warning reduction 的仓库级真值只以同轮 `dotnet clean` 后的 `dotnet build` 为准。
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
1. 推送包含本轮 absolute-path 脱敏的提交后,重新执行 `$gframework-pr-review`,确认 PR `#291` 的 latest-head open thread 是否已自动收口。
|
1. 提交本轮 PR `#297` review follow-up 与 `ai-plan` 同步。
|
||||||
2. 若 PR `#291` 已清零,继续以当前 `639 Warning(s)` 根基线为恢复点,按 `$gframework-batch-boot 50` 规则挑选下一个 1-3 文件的低风险热点。
|
2. 下一波优先挑选 `ArchitectureContextTests.cs` 或 `AsyncQueryExecutorTests.cs` 这类 `7`-warning 的纯 `MA0048` 单文件切片。
|
||||||
3. 若 GitHub 仍保留 review 信号,先确认它们是否仍指向新 head,再决定是否需要继续清理同主题下的其它历史 `ai-plan` 记录。
|
3. 继续将 `YamlConfigSchemaValidator*` 与 `GFramework.Cqrs.Tests/Mediator/*` 作为独立高风险波次处理。
|
||||||
|
|||||||
@ -1,32 +1,87 @@
|
|||||||
# Analyzer Warning Reduction 追踪
|
# Analyzer Warning Reduction 追踪
|
||||||
|
|
||||||
## 2026-04-26 — RP-073
|
## 2026-04-27 — RP-084
|
||||||
|
|
||||||
### 阶段:脱敏 analyzer-warning-reduction 文档中的绝对路径记录
|
### 阶段:收敛 PR #297 的 CodeRabbit follow-up
|
||||||
|
|
||||||
- 触发背景:
|
- 触发背景:
|
||||||
- 用户再次显式要求执行 `$gframework-pr-review`,当前分支仍对应 PR `#291`
|
- 用户执行 `$gframework-pr-review`,要求以当前分支对应 PR 为准,提取并核对 AI review / check 信号
|
||||||
- 最新抓取结果确认 latest-head 还剩 `2` 条 open review thread,分别指向 active todo 与 archive trace 中记录的绝对路径
|
- `fetch_current_pr_review.py` 返回 PR `#297` 的最新 head review 中仍有 `3` 个 open threads,另有 `2` 个 folded nitpick 仍然适用
|
||||||
- active trace 当前也保留了同类 `/tmp` 路径记录;虽然这次 review 没直接点名,但继续保留会留下同一类治理缺口
|
|
||||||
- 主线程实施:
|
- 主线程实施:
|
||||||
- 将 active todo 与 active trace 中的 PR review 输出路径改写为 `--json-output <current-pr-review-json>`
|
- 校验 `GFramework.Game/Config/YamlConfigLoader.cs` 后,保留 `ReadYamlAsync` 的原始取消语义,并把 `IntegerTryParseDelegate<T>` 第一个参数改为 `string?`
|
||||||
- 将 [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md) 里的临时 `dotnet` home、PR review 输出路径和失效 Windows fallback package folder 改写为仓库安全占位符
|
- 校验 `GFramework.Core.Tests/Ioc/*` 与 `Query/TestAsyncQueryWithExceptionV4.cs` 后,补齐缺失 XML 文档,让 `IPrioritizedService` 继承 `IMixedService` 复用 `Name` 契约,并补上 `<returns>` 文档
|
||||||
- 同步刷新 active todo 中的 review 真值,把当前恢复点更新到 `RP-073`
|
- 新增 `YamlConfigLoaderTests.ReadYamlAsync_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested`,用反射直接命中私有读取路径,稳定回归本次取消语义修复
|
||||||
|
- 用 `dotnet format --verify-no-changes --include ...` 清理并验证本次改动文件的格式状态
|
||||||
- 验证里程碑:
|
- 验证里程碑:
|
||||||
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output <current-pr-review-json>`
|
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderTests.ReadYamlAsync_Should_Preserve_OperationCanceledException_When_Cancellation_Is_Requested"`
|
||||||
- 结果:成功;确认 PR `#291` latest-head open review thread 为 `2`,两者都指向 `ai-plan` 文档中的绝对路径记录
|
- 结果:成功;`1` 通过、`0` 失败
|
||||||
- `dotnet build`
|
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests.GetAllByPriority_Should_Sort_By_Priority_Ascending"`
|
||||||
- 结果:成功;`639 Warning(s)`、`0 Error(s)`;与当前权威仓库根基线一致
|
- 结果:成功;`1` 通过、`0` 失败
|
||||||
|
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||||
|
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||||
|
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||||
|
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||||
|
- `dotnet format GFramework.sln --verify-no-changes --include GFramework.Game/Config/YamlConfigLoader.cs GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs GFramework.Core.Tests/Ioc/IMixedService.cs GFramework.Core.Tests/Ioc/IPrioritizedService.cs GFramework.Core.Tests/Ioc/PrioritizedService.cs GFramework.Core.Tests/Query/TestAsyncQueryWithExceptionV4.cs`
|
||||||
|
- 结果:成功
|
||||||
- 当前结论:
|
- 当前结论:
|
||||||
- 本轮只吸收当前仍成立的 PR review 文档项,不扩展到新的 warning 清理切片
|
- PR `#297` 当前仍然有效的 CodeRabbit open threads 与 folded nitpick 已在本地全部核对并收敛
|
||||||
- 当前仓库根 warning 权威基线仍保持 `639 Warning(s)`、`0 Error(s)`;本轮目标是让 analyzer-warning-reduction 主题下当前入口不再记录绝对路径
|
- 当前恢复点完成后,分支可以回到 `ArchitectureContextTests.cs` / `AsyncQueryExecutorTests.cs` / `YamlConfigSchemaValidator*` 的 warning reduction 主线
|
||||||
- 下一轮默认先推送本轮同步并重新执行 `$gframework-pr-review`,确认 PR `#291` 的 open thread 是否已自动收口
|
- 下一步:
|
||||||
|
1. 提交本轮 PR review follow-up。
|
||||||
|
2. 继续执行下一波 `MA0048` 小切片,优先避免一次性进入 `Mediator*` 的高 changed-file 风险波次。
|
||||||
|
|
||||||
|
## 2026-04-27 — RP-083
|
||||||
|
|
||||||
|
### 阶段:修复 `YamlConfigLoader` 单文件 warning,并拆分 `MicrosoftDiContainerTests` 的辅助类型
|
||||||
|
|
||||||
|
- 触发背景:
|
||||||
|
- 用户执行 `$gframework-batch-boot 50`,要求先拿仓库根构建 warning,再按 bounded slice 分派给不同 subagent 并持续推进
|
||||||
|
- 当前分支在本轮开始时与 `origin/main@b6a9fef` 零提交差异,适合从低风险 warning slice 起步
|
||||||
|
- 主线程实施:
|
||||||
|
- 先执行 non-incremental 仓库根基线:`dotnet clean` + `dotnet build`,得到 `397 Warning(s)` / `316` 个唯一位点
|
||||||
|
- 主线程修复 `GFramework.Game/Config/YamlConfigLoader.cs` 的 `MA0051`、`MA0002` 与 `MA0158`
|
||||||
|
- 接受一个 worker batch:将 `GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 末尾的 `10` 个测试辅助接口/类拆分到 `Ioc/` 同目录独立文件
|
||||||
|
- 接受第二波 worker 的已落地结果:将 `GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs` 末尾的 `7` 个测试辅助类型拆分到 `Query/` 同目录独立文件
|
||||||
|
- 启动 `ArchitectureContextTests.cs` 候选 worker,但在共享工作树落地前主动停止,以避免本轮上下文与 review 面积继续膨胀
|
||||||
|
- 验证里程碑:
|
||||||
|
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||||
|
- 结果:成功;`111 Warning(s)`、`0 Error(s)`
|
||||||
|
- 观察:构建输出未再报告 `GFramework.Game/Config/YamlConfigLoader.cs`
|
||||||
|
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
|
||||||
|
- 结果:成功;`0 Warning(s)`、`0 Error(s)`
|
||||||
|
- `dotnet clean`
|
||||||
|
- 结果:成功;刷新最终 non-incremental 仓库根 warning 基线
|
||||||
|
- `dotnet build`
|
||||||
|
- 结果:成功;`353 Warning(s)`、`0 Error(s)`,唯一位点 `279`
|
||||||
|
- 观察:构建输出未再报告 `GFramework.Game/Config/YamlConfigLoader.cs`、`GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 与 `GFramework.Core.Tests/Query/AbstractAsyncQueryTests.cs`
|
||||||
|
- 当前结论:
|
||||||
|
- 本轮已完成一个主线程单文件 slice 和两个 worker 拆分 slice;仓库根 non-incremental warning 从 `397` 降到 `353`
|
||||||
|
- 当前共享工作树 footprint 为 `22` 个 changed files,仍低于 `$gframework-batch-boot 50` 的停止线
|
||||||
|
- 下一波更适合继续处理 `7` 个 `MA0048` 的小文件,而不是立即进入 `Mediator*` 或 `YamlConfigSchemaValidator*` 的高耦合热点
|
||||||
|
|
||||||
|
## 活跃风险
|
||||||
|
|
||||||
|
- `GFramework.Cqrs.Tests/Mediator/*` 的 `MA0048` 位点密度很高,一次性拆分会迅速推高 changed-file 数。
|
||||||
|
- 缓解措施:下一波优先继续拿 `7` warning 级别的小切片。
|
||||||
|
- `YamlConfigSchemaValidator*` 仍然聚集多类高耦合 warning。
|
||||||
|
- 缓解措施:继续维持为独立波次,不与测试项目拆分混提。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
1. 完成本轮 `YamlConfigLoader.cs`、`MicrosoftDiContainerTests.cs` 与 `ai-plan` 的提交。
|
||||||
|
2. 下一波优先从 `ArchitectureContextTests.cs` 或 `AsyncQueryExecutorTests.cs` 继续拆分纯 `MA0048`。
|
||||||
|
|
||||||
## 历史归档指针
|
## 历史归档指针
|
||||||
|
|
||||||
- 最新 trace 归档:
|
- 最新 trace 归档:
|
||||||
|
- [analyzer-warning-reduction-history-rp073-rp078.md](../archive/traces/analyzer-warning-reduction-history-rp073-rp078.md)
|
||||||
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
- [analyzer-warning-reduction-history-rp062-rp071.md](../archive/traces/analyzer-warning-reduction-history-rp062-rp071.md)
|
||||||
- 早期 trace 归档:
|
- 历史 todo 归档:
|
||||||
|
- [analyzer-warning-reduction-history-rp074-rp078.md](../archive/todos/analyzer-warning-reduction-history-rp074-rp078.md)
|
||||||
|
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||||
|
- 早期归档:
|
||||||
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
|
||||||
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||||
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
|
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
|
||||||
|
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
|
||||||
|
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
|
||||||
|
|||||||
@ -12,14 +12,13 @@
|
|||||||
|
|
||||||
## 当前恢复点
|
## 当前恢复点
|
||||||
|
|
||||||
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-040`
|
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-044`
|
||||||
- 当前阶段:`Phase 5 - Governance Maintenance`
|
- 当前阶段:`Phase 5 - Governance Maintenance`
|
||||||
- 当前焦点:
|
- 当前焦点:
|
||||||
- 继续以最新 `origin/main`(`79934f7`,`2026-04-25 16:15:55 +08:00`)作为 baseline,当前批处理 stop condition 仍是 branch diff vs baseline 接近 `50` changed files
|
- 继续以最新 `origin/main`(`617e0bf`,`2026-04-26 12:17:15 +08:00`)作为 baseline,当前批处理 stop condition 仍是 branch diff vs baseline 接近 `50` changed files
|
||||||
- 当前批次已从“单点 review 收口”切到“覆盖整个项目功能的 reader-facing 文档补齐”,重点处理 4 组低风险切片:meta-package / 安装入口、config tool adoption、source-generators 真实契约修正、内部支撑模块 README
|
- 本轮从 `$gframework-pr-review` 重新抓取当前 PR `#296`,确认 latest reviewed commit 为 `5778782df05e22dd24dc95189dd768458afb8537`,剩余 open thread 都落在 reader-facing 文案与 README 导航收口上
|
||||||
- 当前未提交工作树已触达 `18` 个文件,其中 `14` 个更新、`4` 个新增;当前 committed branch diff vs `origin/main` 仍为 `3` files,提交本批次后仍明显低于 `50` 文件阈值
|
- 当前工作树相对 `origin/main` 的 tracked diff 仍接近 `50` files;因此本轮只接受 latest-head review 中仍成立的 4 条低风险修正,不再扩新栏目或新专题页
|
||||||
- 已接受 subagent 结论:`Cqrs` 当前不是栏目缺失,而是 `docs/zh-CN/core/cqrs.md` 需要补 `Request` / stream 变体与协程入口;source-generators 侧当前优先修正文档失真与共享支撑层说明,而不是扩新导航
|
- 已确认 `Title check` 的 inconclusive 仅是 GitHub PR 标题元数据提示,不属于仓库文件内可修复范围;本轮只处理本地仍成立的文档线程
|
||||||
- `2026-04-26` 重新抓取 PR `#292` 后确认 latest reviewed commit 已推进到 `d3d62cf4541063c46458f88eea0f5acd1b4503f9`;当前 open thread 仍集中在 `tools/gframework-config-tool/README.md`,其中“缺少中文文档入口链接”已在本地工作树验证为过期评论,仍需收口的是补最小接入路径以及统一 `current schema subset` 术语
|
|
||||||
|
|
||||||
## 当前状态摘要
|
## 当前状态摘要
|
||||||
|
|
||||||
@ -32,6 +31,14 @@
|
|||||||
- `2026-04-25` `docs/zh-CN/source-generators/index.md` 已按 PR `#292` review 调整“共享支撑模块”段落句式,避免“对读者更重要的判断是”这类拗口表达。
|
- `2026-04-25` `docs/zh-CN/source-generators/index.md` 已按 PR `#292` review 调整“共享支撑模块”段落句式,避免“对读者更重要的判断是”这类拗口表达。
|
||||||
- `2026-04-25` `tools/gframework-config-tool/README.md` 已新增 `Documentation` 章节,直接链接到 `docs/zh-CN/game/config-tool.md` 与 `config-system.md`,让工具 README 能回到完整中文接入文档。
|
- `2026-04-25` `tools/gframework-config-tool/README.md` 已新增 `Documentation` 章节,直接链接到 `docs/zh-CN/game/config-tool.md` 与 `config-system.md`,让工具 README 能回到完整中文接入文档。
|
||||||
- `2026-04-26` `tools/gframework-config-tool/README.md` 已补 `Quick Start`,把安装扩展、配置 `configPath` / `schemasPath`、打开 Explorer、先跑校验、再进入表单 / 批量编辑的最小接入路径串起来,并把 `Validation Coverage` 的 `stable config-schema subset` 统一为 `current schema subset`。
|
- `2026-04-26` `tools/gframework-config-tool/README.md` 已补 `Quick Start`,把安装扩展、配置 `configPath` / `schemasPath`、打开 Explorer、先跑校验、再进入表单 / 批量编辑的最小接入路径串起来,并把 `Validation Coverage` 的 `stable config-schema subset` 统一为 `current schema subset`。
|
||||||
|
- `2026-04-27` `docs/zh-CN/getting-started/installation.md` 已补齐当前公开选包矩阵,新增 `Core.Abstractions`、`Game.Abstractions`、`Ecs.Arch`、`Ecs.Arch.Abstractions` 的 reader-facing 安装说明,并把 `Godot` 常见问题里的旧版 `>= 4.5` 提示收敛到当前 `4.6.2` 基线。
|
||||||
|
- `2026-04-27` `GFramework.Core.Abstractions/README.md`、`GFramework.Game.Abstractions/README.md`、`GFramework.Game.SourceGenerators/README.md`、`GFramework.Ecs.Arch.Abstractions/README.md` 当前都已把 XML 阅读入口改写为“代表类型 + 阅读重点”,不再暴露覆盖计数、日期或 `已覆盖` 这类治理字段。
|
||||||
|
- `2026-04-27` `docs/zh-CN/game/config-system.md` 与 `docs/zh-CN/tutorials/basic/index.md` 已把维护者 / 指挥式措辞改成中性的采用建议与阅读入口,避免公开页面继续暴露内部决策口吻。
|
||||||
|
- `2026-04-27` `docs/zh-CN/getting-started/index.md`、`core/index.md`、`game/index.md`、`api-reference/index.md`、`source-generators/index.md` 已统一收敛为“适用场景 / 起步路线 / 继续阅读”式 reader-facing 入口,不再把 GitHub blob README 或治理说明当作主导航。
|
||||||
|
- `2026-04-27` `GFramework.Game/README.md`、`GFramework.Game.Abstractions/README.md`、`GFramework.Godot/README.md`、`GFramework.Cqrs.Abstractions/README.md`、`GFramework.Ecs.Arch/README.md` 已收口 `ai-libs`、`family`、`seam`、`ReadMe.md` 等内部化或文件名式表述。
|
||||||
|
- `2026-04-27` `docs/zh-CN` 当前已清空所有指向 `github.com/GeWuYou/GFramework/blob/main/.../README.md` 的公开外链,相关入口统一回到站内栏目页、专题页或 API 导航。
|
||||||
|
- `2026-04-27` `docs/zh-CN/tutorials/godot-integration.md`、`game/setting.md`、`game/serialization.md`、`godot/index.md`、`godot/architecture.md`、`godot/storage.md`、`godot/logging.md`、`godot/setting.md`、`godot/extensions.md`、`core/architecture.md` 已把 `旧文档` / `ai-libs` / `.Wait()` / `family` 这类维护与内部语气改写成当前采用说明。
|
||||||
|
- `2026-04-27` 已重新抓取 PR `#296` 并逐条复核 latest-head review:`GFramework.Game.SourceGenerators/README.md` 的 XML 阅读表已改成语义标签,`GFramework.Game/README.md` 已删除重复的 `storage.md` 入口,`docs/zh-CN/tutorials/godot-integration.md` 与 `docs/zh-CN/godot/extensions.md` 已收口仍成立的 reader-facing 措辞问题。
|
||||||
- `2026-04-25` 当前批次已补齐 meta-package / 安装面:`GFramework.csproj` 不再保留占位描述,`README.md`、`docs/zh-CN/index.md`、`docs/zh-CN/getting-started/installation.md` 当前明确说明聚合元包只聚合 `Core` + `Game`,并把安装入口更新到当前 `net8.0/net9.0/net10.0` 与 Godot `4.6.2` 基线。
|
- `2026-04-25` 当前批次已补齐 meta-package / 安装面:`GFramework.csproj` 不再保留占位描述,`README.md`、`docs/zh-CN/index.md`、`docs/zh-CN/getting-started/installation.md` 当前明确说明聚合元包只聚合 `Core` + `Game`,并把安装入口更新到当前 `net8.0/net9.0/net10.0` 与 Godot `4.6.2` 基线。
|
||||||
- `2026-04-25` `docs/zh-CN/game/config-tool.md` 已新增为 reader-facing 工具页,`docs/zh-CN/game/index.md`、`config-system.md`、`docs/.vitepress/config.mts` 与 `tools/gframework-config-tool/README.md` 当前把 VS Code 配置工具纳入 `Game` 配置工作流入口。
|
- `2026-04-25` `docs/zh-CN/game/config-tool.md` 已新增为 reader-facing 工具页,`docs/zh-CN/game/index.md`、`config-system.md`、`docs/.vitepress/config.mts` 与 `tools/gframework-config-tool/README.md` 当前把 VS Code 配置工具纳入 `Game` 配置工作流入口。
|
||||||
- `2026-04-25` source-generators 栏目已修正 4 处真实契约问题:`GetNode` 显式路径 / `Lookup` 语义、枚举生成器实际开关、`Context Get` 集合注入边界,以及 `GFramework.SourceGenerators.Common` / `*.SourceGenerators.Abstractions` 的共享支撑层说明。
|
- `2026-04-25` source-generators 栏目已修正 4 处真实契约问题:`GetNode` 显式路径 / `Lookup` 语义、枚举生成器实际开关、`Context Get` 集合注入边界,以及 `GFramework.SourceGenerators.Common` / `*.SourceGenerators.Abstractions` 的共享支撑层说明。
|
||||||
@ -51,7 +58,7 @@
|
|||||||
`MSB4276` / `MSB4018`;这是已知环境阻塞,不属于本轮文档回归。
|
`MSB4276` / `MSB4018`;这是已知环境阻塞,不属于本轮文档回归。
|
||||||
- 当前 WSL 会话里 `git.exe` 可解析但不能执行,应继续使用显式 `--git-dir` / `--work-tree` 绑定作为默认 Git 策略。
|
- 当前 WSL 会话里 `git.exe` 可解析但不能执行,应继续使用显式 `--git-dir` / `--work-tree` 绑定作为默认 Git 策略。
|
||||||
- `dotnet build GFramework.csproj -c Release` 当前仍会输出仓库既有 analyzer warnings(如 `MA0158`、`MA0051`、`MA0004`);本轮仅修改文档与 package metadata,不扩展到 warning 清理。
|
- `dotnet build GFramework.csproj -c Release` 当前仍会输出仓库既有 analyzer warnings(如 `MA0158`、`MA0051`、`MA0004`);本轮仅修改文档与 package metadata,不扩展到 warning 清理。
|
||||||
- PR `#292` 当前 review 线程仍主要来自 CodeRabbit,对 reader-facing 文案和文档入口连通性要求较细;本轮提交后仍需重新抓取 latest-head review,确认 open thread 是否已自动关闭。
|
- PR `#296` 当前 review 线程仍主要来自 CodeRabbit 与 Greptile,对 reader-facing 文案和文档入口连通性要求较细;本轮提交后仍需重新抓取 latest-head review,确认 open thread 是否已自动关闭。
|
||||||
|
|
||||||
## 归档指针
|
## 归档指针
|
||||||
|
|
||||||
@ -68,12 +75,82 @@
|
|||||||
|
|
||||||
## 最新验证
|
## 最新验证
|
||||||
|
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game.SourceGenerators/README.md GFramework.Game/README.md`
|
||||||
|
- 结果:通过;本轮 2 个 README 的 reader-facing 表格与导航去重调整后链接目标有效。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
|
||||||
|
- 结果:通过;Godot 集成教程的措辞收口后页面 frontmatter、链接与代码块校验均通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
|
||||||
|
- 结果:通过;Godot 扩展页去自我指涉表述后页面 frontmatter、链接与代码块校验均通过。
|
||||||
|
- `2026-04-27` `bun run build`(工作目录:`docs/`)
|
||||||
|
- 结果:通过;本轮 PR `#296` review 收口后的站点仍可构建,仅保留既有大 chunk warning。
|
||||||
|
- `2026-04-27` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
|
||||||
|
- 结果:通过;PR `#296` 处于 `OPEN`,latest head review 共有 `4` 条 open thread,其中 `3` 条文档问题与 `1` 条措辞 nitpick 在本地复核后仍成立;测试汇总为 `2156 passed`,仅剩 `Title check` inconclusive。
|
||||||
- `2026-04-25` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
|
- `2026-04-25` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
|
||||||
- 结果:通过;PR `#290` 处于 `OPEN`,latest head commit `54b8e5770af9ab3c8a86a396ffa4794fe4bb5181` 有 `2` 条 open thread(CodeRabbit `1`、Greptile `1`),测试汇总为 `2156 passed`,无 failed checks。
|
- 结果:通过;PR `#290` 处于 `OPEN`,latest head commit `54b8e5770af9ab3c8a86a396ffa4794fe4bb5181` 有 `2` 条 open thread(CodeRabbit `1`、Greptile `1`),测试汇总为 `2156 passed`,无 failed checks。
|
||||||
- `2026-04-25` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
|
- `2026-04-25` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
|
||||||
- 结果:通过;PR `#292` 处于 `OPEN`,latest head commit `b96565ffa367bade30f44c2d4e8955143fbff85e` 有 `2` 条 CodeRabbit open thread,测试汇总为 `2156 passed`,无 failed tests;另有 `Title check` inconclusive,属于 PR 标题元数据问题,不是仓库文件阻塞。
|
- 结果:通过;PR `#292` 处于 `OPEN`,latest head commit `b96565ffa367bade30f44c2d4e8955143fbff85e` 有 `2` 条 CodeRabbit open thread,测试汇总为 `2156 passed`,无 failed tests;另有 `Title check` inconclusive,属于 PR 标题元数据问题,不是仓库文件阻塞。
|
||||||
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core/README.md GFramework.Ecs.Arch/README.md GFramework.Game/README.md`
|
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core/README.md GFramework.Ecs.Arch/README.md GFramework.Game/README.md`
|
||||||
- 结果:通过;本轮 3 个模块 README 调整后链接目标仍然有效。
|
- 结果:通过;本轮 3 个模块 README 调整后链接目标仍然有效。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core.Abstractions/README.md GFramework.Game.Abstractions/README.md GFramework.Game.SourceGenerators/README.md GFramework.Ecs.Arch.Abstractions/README.md`
|
||||||
|
- 结果:通过;4 个公开模块 README 的 reader-facing 改写后链接目标仍然有效。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/getting-started`
|
||||||
|
- 结果:通过;`installation.md` 更新后 `getting-started` 栏目的 frontmatter、链接与代码块校验均通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/config-system.md`
|
||||||
|
- 结果:通过;`config-system.md` 的工具形态建议改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/basic`
|
||||||
|
- 结果:通过;基础教程入口的阅读路径改写后栏目校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game/README.md GFramework.Game.Abstractions/README.md GFramework.Godot/README.md GFramework.Cqrs.Abstractions/README.md GFramework.Ecs.Arch/README.md`
|
||||||
|
- 结果:通过;本轮 5 个模块 README 的 reader-facing 术语与入口改写后链接目标有效。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/index.md`
|
||||||
|
- 结果:通过;教程页受众表述改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
|
||||||
|
- 结果:通过;Godot UI 页的接法示例与 reader-facing 术语改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
|
||||||
|
- 结果:通过;Godot 场景页的接法示例与 reader-facing 术语改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
|
||||||
|
- 结果:通过;信号页切回站内生成器入口后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions`
|
||||||
|
- 结果:通过;3 个抽象层页改回站内入口后栏目校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators`
|
||||||
|
- 结果:通过;生成器栏目及受影响专题页改回站内入口后栏目校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
|
||||||
|
- 结果:通过;Core 入口页 reader-facing 改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/index.md`
|
||||||
|
- 结果:通过;Game 入口页 reader-facing 改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`
|
||||||
|
- 结果:通过;API 入口页导航改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/getting-started/quick-start.md`
|
||||||
|
- 结果:通过;快速开始页切回站内安装入口后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
|
||||||
|
- 结果:通过;CQRS 页继续阅读入口改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/scene.md`
|
||||||
|
- 结果:通过;Game 场景页相关推荐改回站内入口后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/ui.md`
|
||||||
|
- 结果:通过;Game UI 页相关推荐改回站内入口后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
|
||||||
|
- 结果:通过;ECS Arch 页入口改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
|
||||||
|
- 结果:通过;Godot 集成教程的接线口吻改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/setting.md`
|
||||||
|
- 结果:通过;设置系统页初始化语义改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/serialization.md`
|
||||||
|
- 结果:通过;序列化页生命周期说明改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
|
||||||
|
- 结果:通过;Godot landing page 的采用说明改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
|
||||||
|
- 结果:通过;Godot 架构页异步初始化口吻改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`
|
||||||
|
- 结果:通过;Godot 存储页示例口吻改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
|
||||||
|
- 结果:通过;Godot 日志页 provider 接线说明改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`
|
||||||
|
- 结果:通过;Godot 设置页 applicator 接线口吻改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
|
||||||
|
- 结果:通过;Godot 扩展页边界说明改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/architecture.md`
|
||||||
|
- 结果:通过;Core 架构页旧初始化入口改写后页面校验通过。
|
||||||
|
- `2026-04-27` `bun run build`(工作目录:`docs/`)
|
||||||
|
- 结果:通过;本轮 README、安装页与公开文案改写后站点仍可构建,仅保留既有大 chunk warning。
|
||||||
- `2026-04-25` `bun run build`(工作目录:`docs/`)
|
- `2026-04-25` `bun run build`(工作目录:`docs/`)
|
||||||
- 结果:通过;移除 `api-reference` 侧栏重复项并统一 `source-generators` 标签后站点仍可正常构建,仅保留既有大 chunk warning。
|
- 结果:通过;移除 `api-reference` 侧栏重复项并统一 `source-generators` 标签后站点仍可正常构建,仅保留既有大 chunk warning。
|
||||||
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh README.md GFramework.Core/README.md GFramework.Core.Abstractions/README.md GFramework.Game/README.md GFramework.Game.Abstractions/README.md GFramework.Game.SourceGenerators/README.md GFramework.Ecs.Arch/README.md GFramework.Ecs.Arch.Abstractions/README.md`
|
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh README.md GFramework.Core/README.md GFramework.Core.Abstractions/README.md GFramework.Game/README.md GFramework.Game.Abstractions/README.md GFramework.Game.SourceGenerators/README.md GFramework.Ecs.Arch/README.md GFramework.Ecs.Arch.Abstractions/README.md`
|
||||||
@ -160,11 +237,9 @@
|
|||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
1. 运行 `bun run test`(工作目录:`tools/gframework-config-tool/`),完成本轮 README 收口改动的最小模块验证。
|
1. 提交当前接近阈值的稳定批次后,优先重新抓取 `$gframework-pr-review` 或在新一轮里按 `46 / 50` 的 branch diff 重新评估是否还适合继续扩批。
|
||||||
2. 验证通过后重新抓取 `$gframework-pr-review`,确认 PR `#292` 的 latest-head review 是否只剩过期线程或已自动清空。
|
2. 若后续还要继续文档治理,优先复核尚未触达的 `Game` persistence、Godot runtime 细页与少量残余 `ai-libs` 口吻,而不是继续扩大同一轮 review 面。
|
||||||
3. 若本轮提交后 branch diff vs `origin/main` 仍明显低于 `50` 文件阈值,再决定是否继续追加低风险 reader-facing 文档切片。
|
3. 若后续分支继续调整 `Game` persistence runtime、README 或公共 API,优先复核 `docs/zh-CN/game/data.md`、
|
||||||
4. 若后续继续扩批,优先在已识别但尚未扩写的低风险 reader-facing 方向里选择下一组:config tool 更深的 adoption 示例、首页 / 安装页的进一步选包引导,或其它 repo-visible support surface 的本地说明补齐。
|
|
||||||
5. 若后续分支继续调整 `Game` persistence runtime、README 或公共 API,优先复核 `docs/zh-CN/game/data.md`、
|
|
||||||
`storage.md`、`serialization.md`、`setting.md` 与 landing page 是否仍保持同一套职责边界。
|
`storage.md`、`serialization.md`、`setting.md` 与 landing page 是否仍保持同一套职责边界。
|
||||||
6. 若后续分支继续调整 `Godot` generator 接法,优先复核 `GFramework.Godot.SourceGenerators/README.md`、
|
4. 若后续分支继续调整 `Godot` generator 接法,优先复核 `GFramework.Godot.SourceGenerators/README.md`、
|
||||||
`docs/zh-CN/tutorials/godot-integration.md` 与相关专题页是否仍保持一致。
|
`docs/zh-CN/tutorials/godot-integration.md` 与相关专题页是否仍保持一致。
|
||||||
|
|||||||
@ -1,5 +1,138 @@
|
|||||||
# Documentation Full Coverage Governance Trace
|
# Documentation Full Coverage Governance Trace
|
||||||
|
|
||||||
|
## 2026-04-27
|
||||||
|
|
||||||
|
### 当前恢复点:RP-044
|
||||||
|
|
||||||
|
- 本轮从 `$gframework-pr-review` 重新进入,继续沿用显式 `--git-dir` / `--work-tree` 绑定确认当前分支仍为 `docs/sdk-update-documentation`,并通过 `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json` 抓取当前 PR `#296`。
|
||||||
|
- 抓取结果显示 latest reviewed commit 为 `5778782df05e22dd24dc95189dd768458afb8537`,共有 `4` 条 open thread:`GFramework.Game.SourceGenerators/README.md` 的表头仍带路径视角、`GFramework.Game/README.md` 有重复 `storage.md` 链接、`docs/zh-CN/tutorials/godot-integration.md` 与 `docs/zh-CN/godot/extensions.md` 还有 reader-facing 措辞收口空间。
|
||||||
|
- 本地逐条复核后确认这 `4` 条都仍成立,但都属于低风险文档收口;唯一 failed check `Title check` 只是 PR 标题元数据提示,不属于仓库文件内修复范围。
|
||||||
|
|
||||||
|
### 当前决策(RP-044)
|
||||||
|
|
||||||
|
- 接受 latest-head review 中仍成立的 `4` 条文档修正,不扩展到 review 未指向的其它页面,避免在当前接近 branch-size stop condition 的阶段继续增大 review 面。
|
||||||
|
- 对 README 表格和导航问题,只做 reader-facing 命名与去重;对教程与 Godot 页面,只做措辞收口,不改变现有采用路径与示例结构。
|
||||||
|
- 在同一轮里同步更新 active topic tracking / trace,并在提交前运行最小页面校验、README 链接校验与站点构建。
|
||||||
|
|
||||||
|
### 当前验证(RP-044)
|
||||||
|
|
||||||
|
- PR review 抓取:
|
||||||
|
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
|
||||||
|
- 结果:通过;PR `#296` 处于 `OPEN`,latest head review 共有 `4` 条 open thread,测试汇总为 `2156 passed`,仅剩 `Title check` inconclusive。
|
||||||
|
- README 链接校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game.SourceGenerators/README.md GFramework.Game/README.md`
|
||||||
|
- 结果:通过;本轮 2 个 README 的 reader-facing 表格与导航去重调整后链接目标有效。
|
||||||
|
- 页面校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
|
||||||
|
- 结果:通过;两页 frontmatter、链接与代码块校验均通过。
|
||||||
|
- 站点构建:
|
||||||
|
- `bun run build`(工作目录:`docs/`)
|
||||||
|
- 结果:通过;本轮 PR `#296` review 收口后的站点仍可构建,仅保留既有大 chunk warning。
|
||||||
|
|
||||||
|
### 当前恢复点:RP-043
|
||||||
|
|
||||||
|
- 在提交 `docs(reader-facing): 统一站内入口与公开术语` 后重新计算 branch diff,确认当前工作树继续补一批新文件后已到 `46` changed files,已经接近 `$gframework-batch-boot 50` 的停止线。
|
||||||
|
- 因此本轮最后只接受 10 个还没进入 branch diff 的文件:`tutorials/godot-integration.md`、`game/setting.md`、`game/serialization.md`、`godot/index.md`、`godot/architecture.md`、`godot/storage.md`、`godot/logging.md`、`godot/setting.md`、`godot/extensions.md`、`core/architecture.md`。
|
||||||
|
- 这批文件统一收口的是同一类问题:把 `旧文档`、`ai-libs`、`.Wait()`、`family` 之类维护 / 内部口吻改成当前采用指导,不扩新结构、不重写示例体系。
|
||||||
|
|
||||||
|
### 当前决策(RP-043)
|
||||||
|
|
||||||
|
- 当前 stop condition 已接近阈值,因此这批验证通过后立即停止继续扩批,避免 branch diff 超过 `50` files 或让 review 面退化。
|
||||||
|
- 提交后本轮默认结束;后续若继续,应从 PR review 或剩余未触达的细页重新开一轮,而不是在同一轮里继续堆文件数。
|
||||||
|
|
||||||
|
### 当前验证(RP-043)
|
||||||
|
|
||||||
|
- 单页校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/godot-integration.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/setting.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/serialization.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/index.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/architecture.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/storage.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/logging.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/setting.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/extensions.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/architecture.md`
|
||||||
|
- 结果:通过;本轮 10 个新文件的 frontmatter、链接与代码块校验全部通过。
|
||||||
|
- 站点构建:
|
||||||
|
- `bun run build`(工作目录:`docs/`)
|
||||||
|
- 结果:通过;接近阈值前的最后一批文案收口后站点仍可构建,仅保留既有大 chunk warning。
|
||||||
|
|
||||||
|
### 当前恢复点:RP-042
|
||||||
|
|
||||||
|
- 用户明确要求在当前阈值内循环推进,并允许使用 subagent 降低主线程上下文压力;因此本轮在主线程保留实现与验证,把热点识别委派给 3 个 explorer。
|
||||||
|
- 接受的 subagent 结论主要有三类:
|
||||||
|
- 入口页最划算的改法是统一 reader-facing 骨架,而不是继续保留治理说明或负向 framing。
|
||||||
|
- 若站内已有栏目页与专题页,GitHub blob README 不应继续作为公开文档主导航。
|
||||||
|
- `GFramework.Game` / `Game.Abstractions` / `Godot` 等 README 仍有 `ai-libs`、`family`、`seam`、`ReadMe.md` 等对外不友好的措辞,适合在同一轮里收口。
|
||||||
|
- 基于这些结论,本轮连续落地 3 组低风险切片:入口页 reader-facing 改写、README / Godot 页去内部口吻、剩余 GitHub blob README 外链改回站内入口。
|
||||||
|
|
||||||
|
### 当前决策(RP-042)
|
||||||
|
|
||||||
|
- 继续保持 critical path 本地执行,不让 subagent 直接改文件;subagent 只负责热点排序与问题归类。
|
||||||
|
- stop condition 继续沿用 `origin/main` + `50` changed files;当前工作树相对 baseline 的 tracked diff 已到 `36` files / `500` changed lines,意味着还能再做一小批,但应先提交当前稳定批次。
|
||||||
|
- 当前批次不扩展到新栏目、新导航层或大段内容重写,只做 reader-facing 入口、术语和站内导航连通性收口。
|
||||||
|
|
||||||
|
### 当前验证(RP-042)
|
||||||
|
|
||||||
|
- README 链接校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game/README.md GFramework.Game.Abstractions/README.md GFramework.Godot/README.md GFramework.Cqrs.Abstractions/README.md GFramework.Ecs.Arch/README.md`
|
||||||
|
- 结果:通过;本轮 5 个 README 的 reader-facing 改写后链接目标有效。
|
||||||
|
- 教程 / Godot 页面校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/index.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/ui.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/scene.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/godot/signal.md`
|
||||||
|
- 结果:通过;受影响页面的 frontmatter、链接与代码块校验通过。
|
||||||
|
- 入口与专题页校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/index.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/index.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/api-reference/index.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/getting-started/quick-start.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core/cqrs.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/scene.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/ui.md`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/ecs/arch.md`
|
||||||
|
- 结果:通过;入口页和相关推荐入口改写后页面校验通过。
|
||||||
|
- 栏目级校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/abstractions`
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/source-generators`
|
||||||
|
- 结果:通过;抽象层与生成器栏目改回站内入口后栏目校验通过。
|
||||||
|
- 站点构建:
|
||||||
|
- `bun run build`(工作目录:`docs/`)
|
||||||
|
- 结果:通过;本轮 3 组 reader-facing 文档批次后站点仍可构建,仅保留既有大 chunk warning。
|
||||||
|
|
||||||
|
### 当前恢复点:RP-041
|
||||||
|
|
||||||
|
- 通过 `$gframework-batch-boot 50` 重新进入后,先按仓库规则读取 `AGENTS.md`、`.ai/environment/tools.ai.yaml`、`ai-plan/public/README.md` 与 active topic tracking / trace,并继续使用显式 `--git-dir` / `--work-tree` 绑定确认当前分支仍为 `docs/sdk-update-documentation`。
|
||||||
|
- 使用显式 Git 绑定确认最新 baseline 为 `origin/main` `617e0bf`(`2026-04-26 12:17:15 +08:00`),当前 committed branch diff vs baseline 为 `0` files,因此本轮继续选择低风险、reader-facing 文档切片。
|
||||||
|
- 本轮收敛出的 3 组切片分别是:`installation.md` 的选包矩阵与旧版 Godot 提示、公开 README 的 XML 阅读入口去治理化,以及 `config-system` / 基础教程入口中的维护者口吻改写。
|
||||||
|
|
||||||
|
### 当前决策(RP-041)
|
||||||
|
|
||||||
|
- 不扩展到导航结构或新专题页,只在现有入口上修正 reader-facing 采用路径与表述一致性。
|
||||||
|
- 对公开 README 中的 XML 阅读入口,统一改成“代表类型 + 阅读重点”,不再暴露覆盖计数、日期或 `已覆盖` 这类治理字段。
|
||||||
|
- stop condition 继续沿用 `origin/main` + `50` changed files;本轮工作树相对 baseline 的 tracked diff 为 `9` files / `191` changed lines,仍明显低于阈值。
|
||||||
|
|
||||||
|
### 当前验证(RP-041)
|
||||||
|
|
||||||
|
- README 链接校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core.Abstractions/README.md GFramework.Game.Abstractions/README.md GFramework.Game.SourceGenerators/README.md GFramework.Ecs.Arch.Abstractions/README.md`
|
||||||
|
- 结果:通过;本轮 4 个 README 的 reader-facing 改写后链接目标有效。
|
||||||
|
- 入门栏目校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/getting-started`
|
||||||
|
- 结果:通过;`installation.md` 更新后 `getting-started` 栏目 frontmatter、链接与代码块校验通过。
|
||||||
|
- 配置系统页校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/game/config-system.md`
|
||||||
|
- 结果:通过;工具形态建议改写后页面校验通过。
|
||||||
|
- 基础教程栏目校验:
|
||||||
|
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/basic`
|
||||||
|
- 结果:通过;入口页阅读路径改写后栏目校验通过。
|
||||||
|
- 站点构建:
|
||||||
|
- `bun run build`(工作目录:`docs/`)
|
||||||
|
- 结果:通过;本轮文档批次后站点仍可构建,仅保留既有大 chunk warning。
|
||||||
|
|
||||||
## 2026-04-26
|
## 2026-04-26
|
||||||
|
|
||||||
### 当前恢复点:RP-040
|
### 当前恢复点:RP-040
|
||||||
|
|||||||
@ -97,7 +97,7 @@ public sealed class DiagnosticsFeature
|
|||||||
|
|
||||||
1. 先读本页,确认你是否真的只需要契约层
|
1. 先读本页,确认你是否真的只需要契约层
|
||||||
2. 再看 [Core 模块总览](../core/index.md) 了解默认运行时怎么组织这些契约
|
2. 再看 [Core 模块总览](../core/index.md) 了解默认运行时怎么组织这些契约
|
||||||
3. 回到模块 README:
|
3. 再按需要回到:
|
||||||
- [Core 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
|
- [入门指南](../getting-started/index.md)
|
||||||
- [Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
|
- [Core 模块总览](../core/index.md)
|
||||||
4. 需要统一导航时,再看 [API 参考](../api-reference/index.md)
|
4. 需要统一导航时,再看 [API 参考](../api-reference/index.md)
|
||||||
|
|||||||
@ -89,6 +89,6 @@ var options = new ArchOptions
|
|||||||
|
|
||||||
1. 先读本页,确认你是否真的只需要契约层
|
1. 先读本页,确认你是否真的只需要契约层
|
||||||
2. 如果需要默认实现,再看 [Arch ECS 集成](../ecs/arch.md)
|
2. 如果需要默认实现,再看 [Arch ECS 集成](../ecs/arch.md)
|
||||||
3. 回到对应模块 README:
|
3. 需要统一入口时,再看:
|
||||||
- [ECS 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md)
|
- [ECS 模块总览](../ecs/index.md)
|
||||||
- [Ecs.Arch 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)
|
- [入门指南](../getting-started/index.md)
|
||||||
|
|||||||
@ -116,6 +116,6 @@ public sealed class ContinueGameCommandHandler
|
|||||||
- [设置系统](../game/setting.md)
|
- [设置系统](../game/setting.md)
|
||||||
- [场景系统](../game/scene.md)
|
- [场景系统](../game/scene.md)
|
||||||
- [UI 系统](../game/ui.md)
|
- [UI 系统](../game/ui.md)
|
||||||
4. 需要仓库侧入口时,回到:
|
4. 需要统一入口时,回到:
|
||||||
- [Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
|
- [Game 模块总览](../game/index.md)
|
||||||
- [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
|
- [入门指南](../getting-started/index.md)
|
||||||
|
|||||||
@ -5,35 +5,34 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
|
|||||||
|
|
||||||
# API 参考
|
# API 参考
|
||||||
|
|
||||||
这里不再维护一份脱离源码演化的“伪 API 列表”。
|
这页不是签名索引,而是“先看哪个模块入口、再回哪里读 XML 文档”的导航页。
|
||||||
|
|
||||||
当前 `GFramework` 的 API 参考链路以四类证据协同为准:
|
最常见的阅读顺序是:
|
||||||
|
|
||||||
1. 模块 README:说明包关系、最小接入路径和目录边界
|
1. 先按模块找到对应栏目入口
|
||||||
2. `docs/zh-CN` 专题页:说明采用顺序、生命周期和使用建议
|
2. 再进专题页确认安装、生命周期和推荐接线方式
|
||||||
3. 代码中的 XML 文档:说明公开 / 内部类型和关键成员的契约
|
3. 最后回到源码中的 XML 文档核对具体契约
|
||||||
4. 教程页:说明这些 API 在真实接入路径中的组合方式
|
|
||||||
|
|
||||||
## 阅读顺序
|
## 阅读顺序
|
||||||
|
|
||||||
### 想确认“该装哪个包、先看哪类 API”
|
### 想确认“该装哪个包、先看哪类 API”
|
||||||
|
|
||||||
先读模块 README,再读对应栏目入口页:
|
先读站内入口页:
|
||||||
|
|
||||||
- 入门入口:[入门指南](../getting-started/index.md)
|
- 入门入口:[入门指南](../getting-started/index.md)
|
||||||
- 根模块地图:[仓库总览](https://github.com/GeWuYou/GFramework/blob/main/README.md)
|
- 安装与选包:[安装配置](../getting-started/installation.md)
|
||||||
|
|
||||||
### 想确认“这个功能属于哪个模块”
|
### 想确认“这个功能属于哪个模块”
|
||||||
|
|
||||||
按下面的模块映射进入对应入口:
|
按下面的模块映射进入对应入口:
|
||||||
|
|
||||||
| 模块族 | 模块 README | 站内入口 | XML 文档关注点 |
|
| 模块族 | 先看什么 | 继续深入 | XML 文档关注点 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `Core` / `Core.Abstractions` | [Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)、[Core 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md) | [Core 栏目](../core/index.md)、[Core 抽象层说明](../abstractions/core-abstractions.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
|
| `Core` / `Core.Abstractions` | [Core 模块](../core/index.md) | [Core 抽象层说明](../abstractions/core-abstractions.md)、[快速开始](../getting-started/quick-start.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
|
||||||
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)、[CQRS 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Abstractions/README.md)、[CQRS 源码生成器说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md) | [CQRS 栏目](../core/cqrs.md)、[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md) | request / notification / handler / pipeline / registry / fallback contract |
|
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / registry / fallback contract |
|
||||||
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)、[Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)、[Game 源码生成器说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.SourceGenerators/README.md) | [Game 模块总览](../game/index.md)、[Game 抽象层说明](../abstractions/game-abstractions.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
|
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 模块总览](../game/index.md) | [Game 抽象层说明](../abstractions/game-abstractions.md)、[配置系统](../game/config-system.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
|
||||||
| `Godot` / `Godot.SourceGenerators` | [Godot 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)、[Godot 源码生成器说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md) | [Godot 模块总览](../godot/index.md)、[Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
|
| `Godot` / `Godot.SourceGenerators` | [Godot 模块总览](../godot/index.md) | [Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
|
||||||
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [Ecs.Arch 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)、[ECS 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md) | [ECS 模块总览](../ecs/index.md)、[Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
|
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [ECS 模块总览](../ecs/index.md) | [Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
|
||||||
|
|
||||||
## 先看 XML,还是先看教程
|
## 先看 XML,还是先看教程
|
||||||
|
|
||||||
@ -61,31 +60,23 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
|
|||||||
- 最佳实践:[最佳实践](../best-practices/index.md)
|
- 最佳实践:[最佳实践](../best-practices/index.md)
|
||||||
- 故障排查:[故障排查](../troubleshooting.md)
|
- 故障排查:[故障排查](../troubleshooting.md)
|
||||||
|
|
||||||
## 当前边界
|
## 共享支撑层怎么看
|
||||||
|
|
||||||
- `GFramework.Core.SourceGenerators.Abstractions`
|
- `GFramework.Core.SourceGenerators.Abstractions`
|
||||||
- `GFramework.Godot.SourceGenerators.Abstractions`
|
- `GFramework.Godot.SourceGenerators.Abstractions`
|
||||||
- `GFramework.SourceGenerators.Common`
|
- `GFramework.SourceGenerators.Common`
|
||||||
|
|
||||||
这些目录当前不是独立消费模块,因此不单独维护站内 API 参考入口。它们的公开说明跟随所属模块 README 和
|
这些目录当前不作为独立采用入口;阅读它们时,优先回到所属模块页和 `source-generators` 栏目,再根据需要下钻到具体源码目录。
|
||||||
`source-generators` 栏目维护。
|
|
||||||
|
|
||||||
更准确地说:
|
|
||||||
|
|
||||||
- `*.SourceGenerators.Abstractions`
|
- `*.SourceGenerators.Abstractions`
|
||||||
- 主要定义公开 attribute 和最小契约,供对应生成器与消费端项目共享。
|
- 主要定义公开 attribute 和最小契约,供对应生成器与消费端项目共享。
|
||||||
- `GFramework.SourceGenerators.Common`
|
- `GFramework.SourceGenerators.Common`
|
||||||
- 主要提供共享生成器基类、通用 diagnostics,以及生成方法冲突等跨模块约束。
|
- 主要提供共享生成器基类、通用 diagnostics,以及生成方法冲突等跨模块约束。
|
||||||
|
|
||||||
如果你在 `Core`、`CQRS`、`Game`、`Godot` 的生成器页面里遇到相似的 diagnostics 或冲突语义,优先把它理解为共享
|
|
||||||
支撑层在复用同一套规则,而不是把这些目录当成新的安装入口。
|
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
把本页当成“API 阅读导航”而不是“签名快照”:
|
把本页当成“API 阅读导航”而不是“签名快照”:
|
||||||
|
|
||||||
- 先选模块
|
- 先选模块
|
||||||
- 再进 README 和专题页确认采用路径
|
- 再进专题页确认采用路径
|
||||||
- 最后回到代码里的 XML 文档核对具体契约
|
- 最后回到代码里的 XML 文档核对具体契约
|
||||||
|
|
||||||
当 README、专题页和 XML 文档出现冲突时,以源码和测试所反映的当前实现为准。
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ await architecture.WaitUntilReadyAsync();
|
|||||||
|
|
||||||
## 初始化时机
|
## 初始化时机
|
||||||
|
|
||||||
当前版本不再使用旧文档里的 `Init()` 入口。注册逻辑必须写在:
|
当前 `Architecture` 的注册逻辑必须写在:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
protected override void OnInitialize()
|
protected override void OnInitialize()
|
||||||
@ -77,7 +77,7 @@ protected override void OnInitialize()
|
|||||||
4. 按阶段初始化 `Utility -> Model -> System`
|
4. 按阶段初始化 `Utility -> Model -> System`
|
||||||
5. 进入 `Ready`
|
5. 进入 `Ready`
|
||||||
|
|
||||||
如果你还看到旧示例里写 `protected override void Init()`,那就是过时内容。
|
如果项目里仍保留 `protected override void Init()` 风格的旧代码,应迁移到 `OnInitialize()`。
|
||||||
|
|
||||||
## 组件注册顺序
|
## 组件注册顺序
|
||||||
|
|
||||||
|
|||||||
@ -214,4 +214,4 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
|
|||||||
- 上下文入口:[Context 上下文](./context.md)
|
- 上下文入口:[Context 上下文](./context.md)
|
||||||
- 生成器专题:[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)
|
- 生成器专题:[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)
|
||||||
- 协程接法:[协程系统](./coroutine.md)
|
- 协程接法:[协程系统](./coroutine.md)
|
||||||
- 模块说明:[CQRS 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)
|
- 选包与入口:[入门指南](../getting-started/index.md)
|
||||||
|
|||||||
@ -8,7 +8,7 @@ description: GFramework.Core 与 GFramework.Core.Abstractions 的运行时入口
|
|||||||
`Core` 栏目对应 `GFramework` 的基础运行时层,主要覆盖 `GFramework.Core` 与 `GFramework.Core.Abstractions`,以及与之直接相邻的旧版
|
`Core` 栏目对应 `GFramework` 的基础运行时层,主要覆盖 `GFramework.Core` 与 `GFramework.Core.Abstractions`,以及与之直接相邻的旧版
|
||||||
`Command` / `Query` 执行器和新版 `CQRS` 迁移入口。
|
`Command` / `Query` 执行器和新版 `CQRS` 迁移入口。
|
||||||
|
|
||||||
如果你第一次接入框架,建议先把这里当作“运行时底座说明”,再按需进入 `Game`、`Godot` 或 Source Generators 栏目。
|
如果你第一次接入框架,可以先把这里当作“运行时底座说明”:先确认 `Core` 解决什么问题、最小安装组合是什么,再决定什么时候转向 `CQRS`、`Game`、`Godot` 或源码生成器。
|
||||||
|
|
||||||
## 模块与包关系
|
## 模块与包关系
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
|
|||||||
|
|
||||||
## 栏目覆盖范围
|
## 栏目覆盖范围
|
||||||
|
|
||||||
`Core` 栏目不是旧版“完整框架教程”的镜像,而是当前实现的入口导航。这里的页面按能力域组织:
|
这里的页面按能力域组织,适合按“我要接什么能力”而不是“我要读完所有目录”的方式进入:
|
||||||
|
|
||||||
- 架构与生命周期
|
- 架构与生命周期
|
||||||
- [架构](./architecture.md)
|
- [架构](./architecture.md)
|
||||||
@ -71,7 +71,7 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
|
|||||||
|
|
||||||
如果你已经知道模块归属,但想确认公开类型的契约边界,建议按下面顺序阅读:
|
如果你已经知道模块归属,但想确认公开类型的契约边界,建议按下面顺序阅读:
|
||||||
|
|
||||||
1. 先看[Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md),确认包关系和目录边界
|
1. 先读本页与 [Core 抽象层说明](../abstractions/core-abstractions.md),确认运行时和契约层边界
|
||||||
2. 再看本栏目对应专题页,确认采用顺序、生命周期与推荐接线方式
|
2. 再看本栏目对应专题页,确认采用顺序、生命周期与推荐接线方式
|
||||||
3. 最后回到源码中的 XML 文档,重点核对这些类型族:
|
3. 最后回到源码中的 XML 文档,重点核对这些类型族:
|
||||||
- `Architecture` / `IArchitectureContext`
|
- `Architecture` / `IArchitectureContext`
|
||||||
@ -148,7 +148,8 @@ public sealed class CounterArchitecture : Architecture
|
|||||||
|
|
||||||
## 对应模块入口
|
## 对应模块入口
|
||||||
|
|
||||||
- [Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
|
- [入门指南](../getting-started/index.md)
|
||||||
- [Core 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
|
- [Core 抽象层说明](../abstractions/core-abstractions.md)
|
||||||
|
- [CQRS 运行时](./cqrs.md)
|
||||||
|
- [源码生成器总览](../source-generators/index.md)
|
||||||
- [API 参考入口](../api-reference/index.md)
|
- [API 参考入口](../api-reference/index.md)
|
||||||
- [仓库总览](https://github.com/GeWuYou/GFramework/blob/main/README.md)
|
|
||||||
|
|||||||
@ -140,5 +140,5 @@ ecsModule.Update(deltaTime);
|
|||||||
|
|
||||||
- ECS 模块总览:[ECS 模块总览](./index.md)
|
- ECS 模块总览:[ECS 模块总览](./index.md)
|
||||||
- 抽象契约页:[ECS 抽象层说明](../abstractions/ecs-arch-abstractions.md)
|
- 抽象契约页:[ECS 抽象层说明](../abstractions/ecs-arch-abstractions.md)
|
||||||
- 仓库模块说明:[Ecs.Arch 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)
|
- 选包与接入顺序:[入门指南](../getting-started/index.md)
|
||||||
- 统一 API / XML 导航:[API 参考](../api-reference/index.md)
|
- 统一 API / XML 导航:[API 参考](../api-reference/index.md)
|
||||||
|
|||||||
@ -1021,11 +1021,11 @@ var hotReload = loader.EnableHotReload(
|
|||||||
|
|
||||||
## 工具形态建议
|
## 工具形态建议
|
||||||
|
|
||||||
当前优先采用 `VS Code Extension` 形态即可覆盖仓库中已落地的主要工作流,包括 schema 校验、轻量表单、批量编辑和 raw YAML 回退。
|
对当前仓库已经落地的工作流而言,`VS Code Extension` 形态已经可以覆盖 schema 校验、轻量表单、批量编辑和 raw YAML 回退这条采用路径。
|
||||||
|
|
||||||
如果你的团队出现以下需求,再评估独立 `Config Studio` 会更合适:
|
如果你的团队出现以下需求,再评估独立 `Config Studio` 会更合适:
|
||||||
|
|
||||||
- 主要使用者变成非开发人员,且 VS Code 的安装与使用成本持续阻碍日常维护
|
- 配置维护主要由非开发角色承担,希望进一步降低 VS Code 的安装和使用门槛
|
||||||
- 需要更重的表格视图、跨表可视化关系编辑、复杂审批流或离线发布流程
|
- 需要更重的表格视图、跨表可视化关系编辑、复杂审批流或离线发布流程
|
||||||
- 插件形态已经明显受限于 VS Code Webview / Extension API,而不是 schema 与工作流本身
|
- 插件形态已经明显受限于 VS Code Webview / Extension API,而不是 schema 与工作流本身
|
||||||
- 已经沉淀出稳定的 schema 元数据约定,足以支撑单独工具的长期维护
|
- 已经沉淀出稳定的 schema 元数据约定,足以支撑单独工具的长期维护
|
||||||
|
|||||||
@ -25,7 +25,7 @@ description: GFramework.Game family 的运行时入口、采用顺序与源码
|
|||||||
|
|
||||||
## 栏目覆盖范围
|
## 栏目覆盖范围
|
||||||
|
|
||||||
`Game` 栏目聚焦“游戏项目该怎么接这些运行时能力”,不再保留与当前实现脱节的泛化模块示例。
|
`Game` 栏目聚焦“游戏项目该怎么把这些运行时能力接起来”,适合先按目标判断你要接入的是配置、设置与存档、Scene / UI 路由,还是文件存储与序列化。
|
||||||
|
|
||||||
当前栏目主要入口:
|
当前栏目主要入口:
|
||||||
|
|
||||||
@ -115,20 +115,16 @@ IStorage storage = new FileStorage("GameData", serializer);
|
|||||||
| `GFramework.Game.Abstractions` | `IConfigRegistry`、`ISaveRepository<TSaveData>`、`ISettingsSystem`、`ISceneRouter`、`IUiRouter` | 契约层边界,以及项目中哪些程序集只应依赖接口 |
|
| `GFramework.Game.Abstractions` | `IConfigRegistry`、`ISaveRepository<TSaveData>`、`ISettingsSystem`、`ISceneRouter`、`IUiRouter` | 契约层边界,以及项目中哪些程序集只应依赖接口 |
|
||||||
| `GFramework.Game.SourceGenerators` | `SchemaConfigGenerator`、`ConfigSchemaDiagnostics` | schema 生成入口与诊断模型,确认配置系统的编译期链路 |
|
| `GFramework.Game.SourceGenerators` | `SchemaConfigGenerator`、`ConfigSchemaDiagnostics` | schema 生成入口与诊断模型,确认配置系统的编译期链路 |
|
||||||
|
|
||||||
## 与真实接法的关系
|
## 运行时与生成器如何配合
|
||||||
|
|
||||||
本栏目内容以源码、`*.csproj`、模块说明页与 `ai-libs/` 下已验证的参考接法为准。
|
- 运行时入口主要来自 `GFramework.Game`
|
||||||
|
- 只依赖接口或拆分业务层时,补充 `GFramework.Game.Abstractions`
|
||||||
例如当前文档应优先和以下已验证事实保持一致:
|
- 需要静态内容配置类型和表包装生成时,再追加 `GFramework.Game.SourceGenerators`
|
||||||
|
- 需要编辑器侧内容维护工作流时,再看 [VS Code 配置工具](./config-tool.md)
|
||||||
- 配置系统采用 `YAML + JSON Schema + Source Generator`
|
|
||||||
- 设置持久化通常通过 `UnifiedSettingsDataRepository`
|
|
||||||
- 场景与 UI 路由依赖项目自己的 factory / root,而不是框架替你绑定引擎对象
|
|
||||||
|
|
||||||
如果某个旧页面与这些事实冲突,应以源码和模块说明页为准,并在同一轮里修正文档。
|
|
||||||
|
|
||||||
## 对应模块入口
|
## 对应模块入口
|
||||||
|
|
||||||
- [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
|
- [入门指南](../getting-started/index.md)
|
||||||
- [Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
|
- [Game 抽象层说明](../abstractions/game-abstractions.md)
|
||||||
- [仓库总览](https://github.com/GeWuYou/GFramework/blob/main/README.md)
|
- [源码生成器总览](../source-generators/index.md)
|
||||||
|
- [API 参考入口](../api-reference/index.md)
|
||||||
|
|||||||
@ -258,5 +258,5 @@ await sceneRouter.PopAsync();
|
|||||||
|
|
||||||
1. [Game 模块总览](./index.md)
|
1. [Game 模块总览](./index.md)
|
||||||
2. [UI 系统](./ui.md)
|
2. [UI 系统](./ui.md)
|
||||||
3. [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
|
3. [Game 抽象层说明](../abstractions/game-abstractions.md)
|
||||||
4. [Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
|
4. [API 参考](../api-reference/index.md)
|
||||||
|
|||||||
@ -46,7 +46,7 @@ IRuntimeTypeSerializer runtimeSerializer = new JsonSerializer();
|
|||||||
|
|
||||||
## 配置生命周期
|
## 配置生命周期
|
||||||
|
|
||||||
这部分是当前实现最容易被旧文档说错的地方。
|
这里最需要先确认的是序列化器的配置生命周期。
|
||||||
|
|
||||||
`JsonSerializer` 不会复制你传入的 `JsonSerializerSettings`。它会直接持有并复用这份实例,以及里面的 `Converters` 集合。
|
`JsonSerializer` 不会复制你传入的 `JsonSerializerSettings`。它会直接持有并复用这份实例,以及里面的 `Converters` 集合。
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ description: 以当前 SettingsModel、SettingsSystem 与相关测试为准,
|
|||||||
- `SettingsModel<TRepository>`
|
- `SettingsModel<TRepository>`
|
||||||
- `SettingsSystem`
|
- `SettingsSystem`
|
||||||
|
|
||||||
而不是旧文档里那种“只靠若干 `Get<T>() / Register(...)` 辅助方法就能自动完成一切”的模型。
|
而不是只靠若干 `Get<T>() / Register(...)` 辅助方法就能自动完成一切的模型。
|
||||||
|
|
||||||
## 当前公开入口
|
## 当前公开入口
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ applicator 的职责不是保存数据,而是把设置结果作用到实际运
|
|||||||
|
|
||||||
## 初始化与迁移的真实语义
|
## 初始化与迁移的真实语义
|
||||||
|
|
||||||
`SettingsModel<TRepository>.InitializeAsync()` 的当前行为,比旧文档里“加载一下就好”更严格一些:
|
`SettingsModel<TRepository>.InitializeAsync()` 会按更完整的初始化与迁移顺序工作:
|
||||||
|
|
||||||
- 它会先调用 `ISettingsDataRepository.LoadAllAsync()`
|
- 它会先调用 `ISettingsDataRepository.LoadAllAsync()`
|
||||||
- 再逐个匹配当前模型里已经登记的设置类型
|
- 再逐个匹配当前模型里已经登记的设置类型
|
||||||
|
|||||||
@ -329,5 +329,5 @@ uiRouter.Hide(modalHandle, UiLayer.Modal);
|
|||||||
1. [Game 模块总览](./index.md)
|
1. [Game 模块总览](./index.md)
|
||||||
2. [场景系统](./scene.md)
|
2. [场景系统](./scene.md)
|
||||||
3. [AutoUiPage 生成器](../source-generators/auto-ui-page-generator.md)
|
3. [AutoUiPage 生成器](../source-generators/auto-ui-page-generator.md)
|
||||||
4. [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
|
4. [Game 抽象层说明](../abstractions/game-abstractions.md)
|
||||||
5. [Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
|
5. [API 参考](../api-reference/index.md)
|
||||||
|
|||||||
@ -5,17 +5,19 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
|
|||||||
|
|
||||||
# 入门指南
|
# 入门指南
|
||||||
|
|
||||||
这一部分只回答三个问题:
|
如果你第一次接触 `GFramework`,或者还在判断“应该先装哪些包、先看哪一组文档”,先从这里开始最省时间。
|
||||||
|
|
||||||
1. `GFramework` 由哪些模块组成
|
这组页面的目标只有一个:帮你用最短路径找到适合当前项目的运行时入口、安装组合和下一步阅读顺序。
|
||||||
2. 第一次接入应该从哪个包开始
|
|
||||||
3. 最小可运行路径是什么
|
|
||||||
|
|
||||||
如果你还没决定具体用法,先阅读本栏目;如果你已经明确要用某个模块,直接进入对应模块的说明页会更快。
|
## 适合谁先读本栏
|
||||||
|
|
||||||
## 推荐起步路径
|
- 第一次接入 `GFramework`,还没决定该从 `Core`、`Game`、`Godot` 还是 `CQRS` 开始
|
||||||
|
- 想先确认最小安装组合,再决定是否追加源码生成器
|
||||||
|
- 想先跑通一个可运行骨架,再深入某个专题页
|
||||||
|
|
||||||
### 只想先把架构跑起来
|
## 按目标选择起步路线
|
||||||
|
|
||||||
|
### 基础运行时起步
|
||||||
|
|
||||||
从 `Core` 开始:
|
从 `Core` 开始:
|
||||||
|
|
||||||
@ -33,8 +35,9 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
|
|||||||
|
|
||||||
- [Core 模块总览](../core/index.md)
|
- [Core 模块总览](../core/index.md)
|
||||||
- [快速开始](./quick-start.md)
|
- [快速开始](./quick-start.md)
|
||||||
|
- [安装配置](./installation.md)
|
||||||
|
|
||||||
### 想用新版 CQRS
|
### 新版 CQRS 请求流
|
||||||
|
|
||||||
在 `Core` 基础上补:
|
在 `Core` 基础上补:
|
||||||
|
|
||||||
@ -51,9 +54,9 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
|
|||||||
对应文档:
|
对应文档:
|
||||||
|
|
||||||
- [CQRS 运行时](../core/cqrs.md)
|
- [CQRS 运行时](../core/cqrs.md)
|
||||||
- 模块说明:[CQRS 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)
|
- [安装配置](./installation.md)
|
||||||
|
|
||||||
### 想做游戏运行时
|
### 游戏运行时与内容配置
|
||||||
|
|
||||||
在 `Core` 基础上按需补:
|
在 `Core` 基础上按需补:
|
||||||
|
|
||||||
@ -70,9 +73,10 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
|
|||||||
对应文档:
|
对应文档:
|
||||||
|
|
||||||
- [Game 模块总览](../game/index.md)
|
- [Game 模块总览](../game/index.md)
|
||||||
- 模块说明:[Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
|
- [配置系统](../game/config-system.md)
|
||||||
|
- [安装配置](./installation.md)
|
||||||
|
|
||||||
### 想接入 Godot
|
### Godot 项目接入
|
||||||
|
|
||||||
继续叠加:
|
继续叠加:
|
||||||
|
|
||||||
@ -81,9 +85,10 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
|
|||||||
对应文档:
|
对应文档:
|
||||||
|
|
||||||
- [Godot 模块总览](../godot/index.md)
|
- [Godot 模块总览](../godot/index.md)
|
||||||
- 模块说明:[Godot 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)
|
- [Godot 集成教程](../tutorials/godot-integration.md)
|
||||||
|
- [安装配置](./installation.md)
|
||||||
|
|
||||||
## Source Generators 什么时候装
|
## 什么时候追加源码生成器
|
||||||
|
|
||||||
只在需要编译期生成代码时再装:
|
只在需要编译期生成代码时再装:
|
||||||
|
|
||||||
@ -99,14 +104,15 @@ description: 概览 GFramework 的模块组成、最小接入路径与继续阅
|
|||||||
- 为 CQRS handlers 生成注册表
|
- 为 CQRS handlers 生成注册表
|
||||||
- 生成 Godot 节点、场景和 UI 包装代码
|
- 生成 Godot 节点、场景和 UI 包装代码
|
||||||
|
|
||||||
|
继续阅读:
|
||||||
|
|
||||||
|
- [源码生成器总览](../source-generators/index.md)
|
||||||
|
- [配置系统](../game/config-system.md)
|
||||||
|
- [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)
|
||||||
|
|
||||||
## 建议阅读顺序
|
## 建议阅读顺序
|
||||||
|
|
||||||
1. [快速开始](./quick-start.md)
|
1. [快速开始](./quick-start.md)
|
||||||
2. 你准备使用的模块说明页
|
2. [安装配置](./installation.md)
|
||||||
3. 对应栏目页,例如 `core`、`game`、`godot`
|
3. 按你的目标进入 [基础运行时](../core/index.md)、[游戏运行时](../game/index.md)、[Godot 集成](../godot/index.md) 或 [源码生成器](../source-generators/index.md)
|
||||||
4. 需要更完整示例时,再进入 `tutorials/`
|
4. 需要完整示例时,再进入 [教程总览](../tutorials/index.md)
|
||||||
|
|
||||||
## 注意
|
|
||||||
|
|
||||||
- 旧文档里有一些早期示例已经和当前 API 漂移。本栏目以后只保留经过代码或测试核对的最小路径。
|
|
||||||
- 若根 README、模块 README 与某篇专题页冲突,以模块 README 和当前代码为准。
|
|
||||||
|
|||||||
@ -5,24 +5,28 @@ description: 说明 GFramework 各运行时与 source generator 包的安装选
|
|||||||
|
|
||||||
# 安装配置
|
# 安装配置
|
||||||
|
|
||||||
GFramework 提供多种安装方式,您可以根据项目需求选择合适的包进行安装。
|
GFramework 采用按模块拆分的安装路径。先确认你要接入的是哪一层运行时、抽象层或源码生成器,再决定安装组合,会比直接把所有包一次性带进来更稳妥。
|
||||||
|
|
||||||
## 包选择说明
|
## 包选择说明
|
||||||
|
|
||||||
GFramework 采用模块化设计,不同包提供不同的功能:
|
GFramework 采用模块化设计,不同包提供不同的功能:
|
||||||
|
|
||||||
| 包名 | 说明 | 适用场景 |
|
| 包名 | 说明 | 适用场景 |
|
||||||
|---------------------------------------------|--------------|--------------------------------|
|
| --- | --- | --- |
|
||||||
| `GeWuYou.GFramework` | 聚合元包 | 快速试用、原型开发 |
|
| `GeWuYou.GFramework` | 聚合元包 | 快速试用、原型开发,或先起一个最小运行时骨架 |
|
||||||
| `GeWuYou.GFramework.Core` | 核心框架 | 生产项目推荐 |
|
| `GeWuYou.GFramework.Core` | Core 运行时 | 生产项目推荐的最小运行时起点 |
|
||||||
| `GeWuYou.GFramework.Cqrs` | CQRS runtime | 命令/查询/通知分发与处理器注册 |
|
| `GeWuYou.GFramework.Core.Abstractions` | Core 抽象契约 | 面向接口开发、测试替身、插件化拆分 |
|
||||||
| `GeWuYou.GFramework.Cqrs.Abstractions` | CQRS 抽象契约 | CQRS 契约、handler 接口与共享抽象 |
|
| `GeWuYou.GFramework.Cqrs` | CQRS runtime | 命令 / 查询 / 通知分发与处理器注册 |
|
||||||
| `GeWuYou.GFramework.Game` | 游戏模块 | 需要游戏特定功能 |
|
| `GeWuYou.GFramework.Cqrs.Abstractions` | CQRS 抽象契约 | 共享 request、handler 与 pipeline 契约 |
|
||||||
| `GeWuYou.GFramework.Godot` | Godot集成 | Godot项目必需 |
|
| `GeWuYou.GFramework.Game` | Game 运行时 | 配置、存储、设置、Scene、UI 等游戏层能力 |
|
||||||
| `GeWuYou.GFramework.Core.SourceGenerators` | Core 源码生成器 | `[Log]`、`[ContextAware]`、架构注入等 |
|
| `GeWuYou.GFramework.Game.Abstractions` | Game 抽象契约 | 共享 `IConfigRegistry`、`ISceneRouter`、`IUiRouter` 等接口 |
|
||||||
| `GeWuYou.GFramework.Game.SourceGenerators` | Game 源码生成器 | 配置 schema / 配表生成 |
|
| `GeWuYou.GFramework.Godot` | Godot 集成 | Godot 项目的运行时接线、节点扩展与宿主适配 |
|
||||||
| `GeWuYou.GFramework.Godot.SourceGenerators` | Godot 源码生成器 | Godot 节点、UI、项目元数据生成 |
|
| `GeWuYou.GFramework.Ecs.Arch` | Arch ECS 运行时 | 需要 `UseArch(...)`、默认 `World` 注册与系统桥接 |
|
||||||
| `GeWuYou.GFramework.Cqrs.SourceGenerators` | CQRS 源码生成器 | 处理器注册表生成 |
|
| `GeWuYou.GFramework.Ecs.Arch.Abstractions` | Arch ECS 抽象契约 | 只共享 ECS 模块接口、配置对象与宿主循环边界 |
|
||||||
|
| `GeWuYou.GFramework.Core.SourceGenerators` | Core 源码生成器 | `[Log]`、`[ContextAware]`、架构注入等 |
|
||||||
|
| `GeWuYou.GFramework.Game.SourceGenerators` | Game 源码生成器 | 配置 schema、配置类型、表包装与注册辅助生成 |
|
||||||
|
| `GeWuYou.GFramework.Godot.SourceGenerators` | Godot 源码生成器 | Godot 节点、UI、项目元数据生成 |
|
||||||
|
| `GeWuYou.GFramework.Cqrs.SourceGenerators` | CQRS 源码生成器 | 处理器注册表生成 |
|
||||||
|
|
||||||
当前 NuGet 发布按模块拆分 source generator 包,不存在 `GeWuYou.GFramework.SourceGenerators` 聚合包。
|
当前 NuGet 发布按模块拆分 source generator 包,不存在 `GeWuYou.GFramework.SourceGenerators` 聚合包。
|
||||||
|
|
||||||
@ -33,6 +37,16 @@ GFramework 采用模块化设计,不同包提供不同的功能:
|
|||||||
|
|
||||||
它不会自动带上 `Cqrs`、`Godot` 或任何 `*.SourceGenerators` 包。如果你需要这些能力,请按模块单独安装。
|
它不会自动带上 `Cqrs`、`Godot` 或任何 `*.SourceGenerators` 包。如果你需要这些能力,请按模块单独安装。
|
||||||
|
|
||||||
|
## 推荐组合
|
||||||
|
|
||||||
|
- 最小运行时:`GeWuYou.GFramework.Core` + `GeWuYou.GFramework.Core.Abstractions`
|
||||||
|
- 新版 CQRS:在 Core 基础上追加 `GeWuYou.GFramework.Cqrs` + `GeWuYou.GFramework.Cqrs.Abstractions`
|
||||||
|
- Game 配置工作流:在 Core 基础上追加 `GeWuYou.GFramework.Game` + `GeWuYou.GFramework.Game.Abstractions` + `GeWuYou.GFramework.Game.SourceGenerators`
|
||||||
|
- Godot 项目:在所需运行时基础上追加 `GeWuYou.GFramework.Godot`,需要生成器辅助时再加 `GeWuYou.GFramework.Godot.SourceGenerators`
|
||||||
|
- Arch ECS:直接安装 `GeWuYou.GFramework.Ecs.Arch`;如果只想共享宿主循环或接口边界,可改为 `GeWuYou.GFramework.Ecs.Arch.Abstractions`
|
||||||
|
|
||||||
|
如果你准备采用 AI-First 配置工作流,可以继续阅读 [游戏内容配置系统](../game/config-system.md) 与 [VS Code 配置工具](../game/config-tool.md)。
|
||||||
|
|
||||||
## 安装方式
|
## 安装方式
|
||||||
|
|
||||||
### 1. 使用 .NET CLI(推荐)
|
### 1. 使用 .NET CLI(推荐)
|
||||||
@ -50,6 +64,10 @@ dotnet add package GeWuYou.GFramework.Cqrs.Abstractions
|
|||||||
dotnet add package GeWuYou.GFramework.Game
|
dotnet add package GeWuYou.GFramework.Game
|
||||||
dotnet add package GeWuYou.GFramework.Game.Abstractions
|
dotnet add package GeWuYou.GFramework.Game.Abstractions
|
||||||
|
|
||||||
|
# Arch ECS
|
||||||
|
dotnet add package GeWuYou.GFramework.Ecs.Arch
|
||||||
|
dotnet add package GeWuYou.GFramework.Ecs.Arch.Abstractions
|
||||||
|
|
||||||
# Godot 集成(仅 Godot 项目需要)
|
# Godot 集成(仅 Godot 项目需要)
|
||||||
dotnet add package GeWuYou.GFramework.Godot
|
dotnet add package GeWuYou.GFramework.Godot
|
||||||
|
|
||||||
@ -88,6 +106,10 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
|
|||||||
<!-- 游戏模块 -->
|
<!-- 游戏模块 -->
|
||||||
<PackageReference Include="GeWuYou.GFramework.Game" Version="1.0.0" />
|
<PackageReference Include="GeWuYou.GFramework.Game" Version="1.0.0" />
|
||||||
<PackageReference Include="GeWuYou.GFramework.Game.Abstractions" Version="1.0.0" />
|
<PackageReference Include="GeWuYou.GFramework.Game.Abstractions" Version="1.0.0" />
|
||||||
|
|
||||||
|
<!-- Arch ECS -->
|
||||||
|
<PackageReference Include="GeWuYou.GFramework.Ecs.Arch" Version="1.0.0" />
|
||||||
|
<PackageReference Include="GeWuYou.GFramework.Ecs.Arch.Abstractions" Version="1.0.0" />
|
||||||
|
|
||||||
<!-- Godot 集成 -->
|
<!-- Godot 集成 -->
|
||||||
<PackageReference Include="GeWuYou.GFramework.Godot" Version="1.0.0" />
|
<PackageReference Include="GeWuYou.GFramework.Godot" Version="1.0.0" />
|
||||||
@ -143,10 +165,9 @@ dotnet add package GeWuYou.GFramework.Cqrs.SourceGenerators
|
|||||||
如果你想排除局部导入,可以继续在项目文件中添加排除项:
|
如果你想排除局部导入,可以继续在项目文件中添加排除项:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<GFrameworkExcludedUsing Include="GFramework.Core.Environment"/>
|
<GFrameworkExcludedUsing Include="GFramework.Core.Environment" />
|
||||||
<GFrameworkExcludedUsing Include="GFramework.Godot.Extensions"/>
|
<GFrameworkExcludedUsing Include="GFramework.Godot.Extensions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -223,7 +244,7 @@ dotnet build
|
|||||||
|
|
||||||
确保:
|
确保:
|
||||||
|
|
||||||
- Godot 版本 >= 4.5
|
- 项目环境与当前文档保持在 Godot 4.6.2 基线
|
||||||
- 已正确安装 Godot C# 模板
|
- 已正确安装 Godot C# 模板
|
||||||
- 项目引用了正确的 Godot 包
|
- 项目引用了正确的 Godot 包
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user