mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63b1d71a0e | ||
|
|
cdc49c319a | ||
|
|
d94d8deb29 | ||
|
|
49609d3821 | ||
|
|
003fe42ad8 | ||
|
|
a42ec0c282 | ||
|
|
884249649d | ||
|
|
d582dffe40 | ||
|
|
63a6c2e6f0 | ||
|
|
f3d45169cd | ||
|
|
86645d34cb | ||
|
|
ab04f0ace7 | ||
|
|
51492b1dcd | ||
|
|
4caa3c0d71 | ||
|
|
fca3808657 | ||
|
|
5996ecf5f3 | ||
|
|
53edd13f8f | ||
|
|
0442fec2d1 | ||
|
|
2001eddbff | ||
|
|
3130a3bab2 | ||
|
|
ba4ee24ce7 | ||
|
|
dfae4ba207 | ||
|
|
ccb51791a3 | ||
|
|
55234c4d70 | ||
|
|
108bcbf27e | ||
|
|
51393c30ee | ||
|
|
589f9f7d63 | ||
|
|
ba8369c8b3 | ||
|
|
9ca28a44d8 | ||
|
|
0c5c9dceae | ||
|
|
d7e7d3cc7f | ||
|
|
b5c67850ce | ||
|
|
1f680d2822 | ||
|
|
075d397a4c | ||
|
|
5fb96761a3 | ||
|
|
e49713a842 | ||
|
|
aee13c3c1d | ||
|
|
05c4f06717 | ||
|
|
2accbf4bdf | ||
|
|
60068aff4f | ||
|
|
9c69c4ec00 | ||
|
|
65b949b62f | ||
|
|
4afa856fdc | ||
|
|
27858df94e | ||
|
|
1c2e68fc5a | ||
|
|
3d8e19b5e2 |
62
.ai/environment/tools.ai.yaml
Normal file
62
.ai/environment/tools.ai.yaml
Normal file
@ -0,0 +1,62 @@
|
||||
schema_version: 1
|
||||
generated_at_utc: "2026-03-21T04:47:58Z"
|
||||
generated_from: ".ai/environment/tools.raw.yaml"
|
||||
generator: "scripts/generate-ai-environment.py"
|
||||
platform:
|
||||
family: "wsl-linux"
|
||||
os: "Linux"
|
||||
distro: "Ubuntu 24.04.4 LTS"
|
||||
shell: "bash"
|
||||
capabilities:
|
||||
dotnet: true
|
||||
python: true
|
||||
node: true
|
||||
bun: true
|
||||
docker: true
|
||||
fast_search: true
|
||||
json_cli: true
|
||||
tool_selection:
|
||||
search:
|
||||
preferred: "rg"
|
||||
fallback: "grep"
|
||||
use_for: "Repository text search."
|
||||
json:
|
||||
preferred: "jq"
|
||||
fallback: "python3"
|
||||
use_for: "Inspecting or transforming JSON command output."
|
||||
shell:
|
||||
preferred: "bash"
|
||||
fallback: "sh"
|
||||
use_for: "Repository shell scripts and command execution."
|
||||
scripting:
|
||||
preferred: "python3"
|
||||
fallback: "bash"
|
||||
use_for: "Non-trivial local automation and helper scripts."
|
||||
docs_package_manager:
|
||||
preferred: "bun"
|
||||
fallback: "npm"
|
||||
use_for: "Installing and previewing the docs site."
|
||||
build_and_test:
|
||||
preferred: "dotnet"
|
||||
fallback: "unavailable"
|
||||
use_for: "Build, test, restore, and solution validation."
|
||||
python:
|
||||
available: true
|
||||
helper_packages:
|
||||
requests: true
|
||||
rich: true
|
||||
openai: false
|
||||
tiktoken: false
|
||||
pydantic: false
|
||||
pytest: false
|
||||
preferences:
|
||||
prefer_project_listed_tools: true
|
||||
prefer_python_for_non_trivial_automation: true
|
||||
avoid_unlisted_system_tools: true
|
||||
rules:
|
||||
- "Use rg instead of grep for repository search when rg is available."
|
||||
- "Use jq for JSON inspection; fall back to python3 if jq is unavailable."
|
||||
- "Prefer python3 over complex bash for non-trivial scripting when python3 is available."
|
||||
- "Use bun for docs preview workflows when bun is available; otherwise fall back to npm."
|
||||
- "Use dotnet for repository build and test workflows."
|
||||
- "Do not assume unrelated system tools are part of the supported project environment."
|
||||
89
.ai/environment/tools.raw.yaml
Normal file
89
.ai/environment/tools.raw.yaml
Normal file
@ -0,0 +1,89 @@
|
||||
schema_version: 1
|
||||
generated_at_utc: "2026-03-21T04:47:28Z"
|
||||
generator: "scripts/collect-dev-environment.sh"
|
||||
|
||||
platform:
|
||||
os: "Linux"
|
||||
distro: "Ubuntu 24.04.4 LTS"
|
||||
version: "24.04"
|
||||
kernel: "5.15.167.4-microsoft-standard-WSL2"
|
||||
wsl: true
|
||||
wsl_version: "2.4.13"
|
||||
shell: "bash"
|
||||
|
||||
required_runtimes:
|
||||
dotnet:
|
||||
installed: true
|
||||
version: "10.0.104"
|
||||
path: "/usr/bin/dotnet"
|
||||
purpose: "Builds and tests the GFramework solution."
|
||||
python3:
|
||||
installed: true
|
||||
version: "Python 3.12.3"
|
||||
path: "/usr/bin/python3"
|
||||
purpose: "Runs local automation and environment collection scripts."
|
||||
node:
|
||||
installed: true
|
||||
version: "v20.20.1"
|
||||
path: "/usr/bin/node"
|
||||
purpose: "Provides the JavaScript runtime used by docs tooling."
|
||||
bun:
|
||||
installed: true
|
||||
version: "1.3.10"
|
||||
path: "/root/.bun/bin/bun"
|
||||
purpose: "Installs and previews the VitePress documentation site."
|
||||
|
||||
required_tools:
|
||||
git:
|
||||
installed: true
|
||||
version: "git version 2.43.0"
|
||||
path: "/usr/bin/git"
|
||||
purpose: "Source control and patch review."
|
||||
bash:
|
||||
installed: true
|
||||
version: "GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)"
|
||||
path: "/usr/bin/bash"
|
||||
purpose: "Executes repository scripts and shell automation."
|
||||
rg:
|
||||
installed: true
|
||||
version: "ripgrep 15.1.0 (rev af60c2de9d)"
|
||||
path: "/root/.bun/install/global/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/path/rg"
|
||||
purpose: "Fast text search across the repository."
|
||||
jq:
|
||||
installed: true
|
||||
version: "jq-1.7"
|
||||
path: "/usr/bin/jq"
|
||||
purpose: "Inspecting and transforming JSON outputs."
|
||||
|
||||
project_tools:
|
||||
docker:
|
||||
installed: true
|
||||
version: "Docker version 29.2.1, build a5c7197"
|
||||
path: "/usr/bin/docker"
|
||||
purpose: "Runs MegaLinter and other containerized validation tools."
|
||||
|
||||
python_packages:
|
||||
requests:
|
||||
installed: true
|
||||
version: "2.31.0"
|
||||
purpose: "Simple HTTP calls in local helper scripts."
|
||||
rich:
|
||||
installed: true
|
||||
version: "13.7.1"
|
||||
purpose: "Readable CLI output for local Python helpers."
|
||||
openai:
|
||||
installed: false
|
||||
version: "not-installed"
|
||||
purpose: "Optional scripted access to OpenAI APIs."
|
||||
tiktoken:
|
||||
installed: false
|
||||
version: "not-installed"
|
||||
purpose: "Optional token counting for prompt and context inspection."
|
||||
pydantic:
|
||||
installed: false
|
||||
version: "not-installed"
|
||||
purpose: "Optional typed config and schema validation for helper scripts."
|
||||
pytest:
|
||||
installed: false
|
||||
version: "not-installed"
|
||||
purpose: "Optional lightweight testing for Python helper scripts."
|
||||
50
.github/workflows/ci.yml
vendored
50
.github/workflows/ci.yml
vendored
@ -13,8 +13,9 @@ permissions:
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Build and Test
|
||||
# 代码质量检查 job(并行执行,不阻塞构建)
|
||||
code-quality:
|
||||
name: Code Quality & Security
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@ -23,9 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# 校验C#命名空间与源码目录是否符合命名规范
|
||||
- name: Validate C# naming
|
||||
run: bash scripts/validate-csharp-naming.sh
|
||||
|
||||
# 缓存MegaLinter
|
||||
- name: Cache MegaLinter
|
||||
uses: actions/cache@v5
|
||||
@ -34,8 +37,7 @@ jobs:
|
||||
key: ${{ runner.os }}-megalinter-v9
|
||||
restore-keys: |
|
||||
${{ runner.os }}-megalinter-
|
||||
|
||||
|
||||
|
||||
# MegaLinter扫描步骤
|
||||
# 执行代码质量检查和安全扫描,生成SARIF格式报告
|
||||
- name: MegaLinter
|
||||
@ -44,11 +46,13 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FAIL_ON_ERROR: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
# 上传SARIF格式的安全和代码质量问题报告到GitHub安全中心
|
||||
- name: Upload SARIF
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: megalinter-reports/sarif
|
||||
|
||||
# 缓存TruffleHog
|
||||
- name: Cache TruffleHog
|
||||
uses: actions/cache@v5
|
||||
@ -68,6 +72,18 @@ jobs:
|
||||
base: ${{ github.event.before }}
|
||||
# 当前提交哈希,作为扫描的目标版本
|
||||
head: ${{ github.sha }}
|
||||
|
||||
# 构建和测试 job(并行执行)
|
||||
build-and-test:
|
||||
name: Build and Test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# 检出源代码
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# 安装和配置.NET SDK版本
|
||||
- name: Setup .NET 8
|
||||
@ -113,29 +129,35 @@ jobs:
|
||||
run: dotnet build -c Release --no-restore
|
||||
|
||||
# 运行单元测试,输出TRX格式结果到TestResults目录
|
||||
- name: Test - Core
|
||||
# 在同一个 step 中并发执行所有测试以加快速度
|
||||
- name: Test All Projects
|
||||
run: |
|
||||
dotnet test GFramework.Core.Tests \
|
||||
-c Release \
|
||||
--no-build \
|
||||
--logger "trx;LogFileName=core-$RANDOM.trx" \
|
||||
--results-directory TestResults
|
||||
|
||||
- name: Test - SourceGenerators
|
||||
run: |
|
||||
--results-directory TestResults &
|
||||
|
||||
dotnet test GFramework.Game.Tests \
|
||||
-c Release \
|
||||
--no-build \
|
||||
--logger "trx;LogFileName=game-$RANDOM.trx" \
|
||||
--results-directory TestResults &
|
||||
|
||||
dotnet test GFramework.SourceGenerators.Tests \
|
||||
-c Release \
|
||||
--no-build \
|
||||
--logger "trx;LogFileName=sg-$RANDOM.trx" \
|
||||
--results-directory TestResults
|
||||
|
||||
- name: Test - GFramework.Ecs.Arch.Tests
|
||||
run: |
|
||||
--results-directory TestResults &
|
||||
|
||||
dotnet test GFramework.Ecs.Arch.Tests \
|
||||
-c Release \
|
||||
--no-build \
|
||||
--logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \
|
||||
--results-directory TestResults
|
||||
--results-directory TestResults &
|
||||
|
||||
# 等待所有后台测试完成
|
||||
wait
|
||||
|
||||
- name: Generate CTRF report
|
||||
run: |
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -12,4 +12,6 @@ opencode.json
|
||||
.omc/
|
||||
docs/.omc/
|
||||
docs/.vitepress/cache/
|
||||
local-plan/
|
||||
local-plan/
|
||||
# tool
|
||||
.venv/
|
||||
220
AGENTS.md
Normal file
220
AGENTS.md
Normal file
@ -0,0 +1,220 @@
|
||||
# AGENTS.md
|
||||
|
||||
This document is the single source of truth for coding behavior in this repository.
|
||||
|
||||
All AI agents and contributors must follow these rules when writing, reviewing, or modifying code in `GFramework`.
|
||||
|
||||
## Environment Capability Inventory
|
||||
|
||||
- Before choosing runtimes or CLI tools, read `@.ai/environment/tools.ai.yaml`.
|
||||
- Use `@.ai/environment/tools.raw.yaml` only when you need the full collected facts behind the AI-facing hints.
|
||||
- Prefer the project-relevant tools listed there instead of assuming every installed system tool is fair game.
|
||||
- If the real environment differs from the inventory, use the project-relevant installed tool and report the mismatch.
|
||||
|
||||
## Commenting Rules (MUST)
|
||||
|
||||
All generated or modified code MUST include clear and meaningful comments where required by the rules below.
|
||||
|
||||
### XML Documentation (Required)
|
||||
|
||||
- All public, protected, and internal types and members MUST include XML documentation comments (`///`).
|
||||
- Use `<summary>`, `<param>`, `<returns>`, `<exception>`, and `<remarks>` where applicable.
|
||||
- Comments must explain intent, contract, and usage constraints instead of restating syntax.
|
||||
- If a member participates in lifecycle, threading, registration, or disposal behavior, document that behavior
|
||||
explicitly.
|
||||
|
||||
### Inline Comments
|
||||
|
||||
- Add inline comments for:
|
||||
- Non-trivial logic
|
||||
- Concurrency or threading behavior
|
||||
- Performance-sensitive paths
|
||||
- Workarounds, compatibility constraints, or edge cases
|
||||
- Registration order, lifecycle sequencing, or generated code assumptions
|
||||
- Avoid obvious comments such as `// increment i`.
|
||||
|
||||
### Architecture-Level Comments
|
||||
|
||||
- Core framework components such as Architecture, Module, System, Context, Registry, Service Module, and Lifecycle types
|
||||
MUST include high-level explanations of:
|
||||
- Responsibilities
|
||||
- Lifecycle
|
||||
- Interaction with other components
|
||||
- Why the abstraction exists
|
||||
- When to use it instead of alternatives
|
||||
|
||||
### Source Generator Comments
|
||||
|
||||
- Generated logic and generator pipelines MUST explain:
|
||||
- What is generated
|
||||
- Why it is generated
|
||||
- The semantic assumptions the generator relies on
|
||||
- Any diagnostics or fallback behavior
|
||||
|
||||
### Complex Logic Requirement
|
||||
|
||||
- Methods with non-trivial logic MUST document:
|
||||
- The core idea
|
||||
- Key decisions
|
||||
- Edge case handling, if any
|
||||
|
||||
### Quality Rules
|
||||
|
||||
- Comments MUST NOT be trivial, redundant, or misleading.
|
||||
- Prefer explaining `why` and `when`, not just `what`.
|
||||
- Code should remain understandable without requiring external context.
|
||||
- Prefer slightly more explanation over too little for framework code.
|
||||
|
||||
### Enforcement
|
||||
|
||||
- Missing required documentation is a coding standards violation.
|
||||
- Code that does not meet the documentation rules is considered incomplete.
|
||||
|
||||
## Code Style
|
||||
|
||||
### Language and Project Settings
|
||||
|
||||
- Follow the repository defaults:
|
||||
- `ImplicitUsings` disabled
|
||||
- `Nullable` enabled
|
||||
- `GenerateDocumentationFile` enabled for shipped libraries
|
||||
- `LangVersion` is generally `preview` in the main libraries and abstractions
|
||||
- Do not rely on implicit imports. Declare every required `using` explicitly.
|
||||
- Write null-safe code that respects nullable annotations instead of suppressing warnings by default.
|
||||
|
||||
### Naming and Structure
|
||||
|
||||
- Use the namespace pattern `GFramework.{Module}.{Feature}` with PascalCase segments.
|
||||
- Follow standard C# naming:
|
||||
- Types, methods, properties, events, and constants: PascalCase
|
||||
- Interfaces: `I` prefix
|
||||
- Parameters and locals: camelCase
|
||||
- Private fields: `_camelCase`
|
||||
- Keep abstractions projects free of implementation details and engine-specific dependencies.
|
||||
- Preserve existing module boundaries. Do not introduce new cross-module dependencies without clear architectural need.
|
||||
|
||||
### Formatting
|
||||
|
||||
- Use 4 spaces for indentation. Do not use tabs.
|
||||
- Use Allman braces.
|
||||
- Keep `using` directives at the top of the file and sort them consistently.
|
||||
- Separate logical blocks with blank lines when it improves readability.
|
||||
- Prefer one primary type per file unless the surrounding project already uses a different local pattern.
|
||||
- Keep line length readable. Around 120 characters is the preferred upper bound.
|
||||
|
||||
### C# Conventions
|
||||
|
||||
- Prefer explicit, readable code over clever shorthand in framework internals.
|
||||
- Match existing async patterns and naming conventions (`Async` suffix for asynchronous methods).
|
||||
- Avoid hidden side effects in property getters, constructors, and registration helpers.
|
||||
- Preserve deterministic behavior in registries, lifecycle orchestration, and generated outputs.
|
||||
- When adding analyzers or suppressions, keep them minimal and justify them in code comments if the reason is not
|
||||
obvious.
|
||||
|
||||
### Analyzer and Validation Expectations
|
||||
|
||||
- The repository uses `Meziantou.Analyzer`; treat analyzer feedback as part of the coding standard.
|
||||
- Naming must remain compatible with `scripts/validate-csharp-naming.sh`.
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Required Coverage
|
||||
|
||||
- Every non-trivial feature, bug fix, or behavior change MUST include tests or an explicit justification for why a test
|
||||
is not practical.
|
||||
- Public API changes must be covered by unit or integration tests.
|
||||
- Regression fixes should include a test that fails before the fix and passes after it.
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Mirror the source structure in test projects whenever practical.
|
||||
- Reuse existing architecture test infrastructure when relevant:
|
||||
- `ArchitectureTestsBase<T>`
|
||||
- `SyncTestArchitecture`
|
||||
- `AsyncTestArchitecture`
|
||||
- Keep tests focused on observable behavior, not implementation trivia.
|
||||
|
||||
### Source Generator Tests
|
||||
|
||||
- Source generator changes MUST be covered by generator tests.
|
||||
- Preserve snapshot-based verification patterns already used in the repository.
|
||||
- When generator behavior changes intentionally, update snapshots together with the implementation.
|
||||
|
||||
### Validation Commands
|
||||
|
||||
Use the smallest command set that proves the change, then expand if the change is cross-cutting.
|
||||
|
||||
```bash
|
||||
# Build the full solution
|
||||
dotnet build GFramework.sln -c Release
|
||||
|
||||
# Run all tests
|
||||
dotnet test GFramework.sln -c Release
|
||||
|
||||
# Run a single test project
|
||||
dotnet test GFramework.Core.Tests -c Release
|
||||
dotnet test GFramework.Game.Tests -c Release
|
||||
dotnet test GFramework.SourceGenerators.Tests -c Release
|
||||
dotnet test GFramework.Ecs.Arch.Tests -c Release
|
||||
|
||||
# Run a single NUnit test or test group
|
||||
dotnet test GFramework.Core.Tests -c Release --filter "FullyQualifiedName~CommandExecutorTests.Execute"
|
||||
|
||||
# Validate naming rules used by CI
|
||||
bash scripts/validate-csharp-naming.sh
|
||||
```
|
||||
|
||||
### Test Execution Expectations
|
||||
|
||||
- Run targeted tests for the code you changed whenever possible.
|
||||
- Run broader solution-level validation for changes that touch shared abstractions, lifecycle behavior, source
|
||||
generators, or dependency wiring.
|
||||
- Do not claim completion if required tests were skipped; state what was not run and why.
|
||||
|
||||
## Security Rules
|
||||
|
||||
- Validate external or user-controlled input before it reaches file system, serialization, reflection, code generation,
|
||||
or process boundaries.
|
||||
- Do not build command strings, file paths, type names, or generated code from untrusted input without strict validation
|
||||
or allow-listing.
|
||||
- Avoid logging secrets, tokens, credentials, or machine-specific sensitive data.
|
||||
- Keep source generators deterministic and free of hidden environment or network dependencies.
|
||||
- Prefer least-privilege behavior for file, process, and environment access.
|
||||
- Do not introduce unsafe deserialization, broad reflection-based activation, or dynamic code execution unless it is
|
||||
explicitly required and tightly constrained.
|
||||
- When adding caching, pooling, or shared mutable state, document thread-safety assumptions and failure modes.
|
||||
- Minimize new package dependencies. Add them only when necessary and keep scope narrow.
|
||||
|
||||
## Documentation Rules
|
||||
|
||||
### Code Documentation
|
||||
|
||||
- Any change to public API, lifecycle semantics, module behavior, or extension points MUST update the related XML docs.
|
||||
- If a framework abstraction changes meaning or intended usage, update the explanatory comments in code as part of the
|
||||
same change.
|
||||
|
||||
### Repository Documentation
|
||||
|
||||
- Update the relevant `README.md` or `docs/` page when behavior, setup steps, architecture guidance, or user-facing
|
||||
examples change.
|
||||
- The main documentation site lives under `docs/`, with Chinese content under `docs/zh-CN/`.
|
||||
- Keep code samples, package names, and command examples aligned with the current repository state.
|
||||
- Prefer documenting behavior and design intent, not only API surface.
|
||||
|
||||
### Documentation Preview
|
||||
|
||||
When documentation changes need local preview, use:
|
||||
|
||||
```bash
|
||||
cd docs && bun install && bun run dev
|
||||
```
|
||||
|
||||
## Review Standard
|
||||
|
||||
Before considering work complete, confirm:
|
||||
|
||||
- Required comments and XML docs are present
|
||||
- Code follows repository style and naming rules
|
||||
- Relevant tests were added or updated
|
||||
- Sensitive or unsafe behavior was not introduced
|
||||
- User-facing documentation is updated when needed
|
||||
134
CLAUDE.md
Normal file
134
CLAUDE.md
Normal file
@ -0,0 +1,134 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides project understanding for AI agents working in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
GFramework 是面向游戏开发的模块化 C# 框架,核心能力与引擎解耦。项目灵感参考 QFramework,并在模块边界、工程组织和可扩展性方面持续重构。
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
All coding rules are defined in:
|
||||
|
||||
@AGENTS.md
|
||||
|
||||
Follow them strictly.
|
||||
|
||||
## Module Dependency Graph
|
||||
|
||||
```text
|
||||
GFramework (meta package) ─→ Core + Game
|
||||
GFramework.Core ─→ Core.Abstractions
|
||||
GFramework.Game ─→ Game.Abstractions, Core, Core.Abstractions
|
||||
GFramework.Godot ─→ Core, Game, Core.Abstractions, Game.Abstractions
|
||||
GFramework.Ecs.Arch ─→ Ecs.Arch.Abstractions, Core, Core.Abstractions
|
||||
GFramework.SourceGenerators ─→ SourceGenerators.Common, SourceGenerators.Abstractions
|
||||
```
|
||||
|
||||
- **Abstractions projects** (`netstandard2.1`): 只包含接口和契约定义,不承载运行时实现逻辑。
|
||||
- **Core / Game / Ecs.Arch** (`net8.0;net9.0;net10.0`): 平台无关的核心实现层。
|
||||
- **Godot**: Godot 引擎集成层,负责与节点、场景和引擎生命周期对接。
|
||||
- **SourceGenerators** (`netstandard2.1`): Roslyn 增量源码生成器及其公共基础设施。
|
||||
|
||||
## Architecture Pattern
|
||||
|
||||
框架核心采用 `Architecture / Model / System / Utility` 四层结构:
|
||||
|
||||
- **IArchitecture**: 顶层容器,负责生命周期管理、组件注册、模块安装和统一服务访问。
|
||||
- **IContextAware**: 统一上下文访问接口,组件通过 `SetContext(IArchitectureContext)` 获取架构上下文。
|
||||
- **IModel**: 数据与状态层,负责长期状态和业务数据建模。
|
||||
- **ISystem**: 业务逻辑层,负责命令执行、流程编排和规则落地。
|
||||
- **IUtility**: 通用无状态工具层,供其他层复用。
|
||||
|
||||
关键实现位于 `GFramework.Core/Architectures/Architecture.cs`,其职责是作为总协调器串联生命周期、组件注册和模块系统。
|
||||
|
||||
## Architecture Details
|
||||
|
||||
### Lifecycle
|
||||
|
||||
Architecture 负责统一生命周期编排,核心阶段包括:
|
||||
|
||||
- `Init`
|
||||
- `Ready`
|
||||
- `Destroy`
|
||||
|
||||
在实现层中,生命周期被拆分为更细粒度的初始化与销毁阶段,用于保证 Utility、Model、System、服务模块和钩子的顺序一致性。
|
||||
|
||||
### Component Coordination
|
||||
|
||||
框架通过独立组件协作完成架构编排:
|
||||
|
||||
- `ArchitectureLifecycle`: 管理生命周期阶段、阶段转换和生命周期钩子。
|
||||
- `ArchitectureComponentRegistry`: 管理 Model、System、Utility 的注册与解析。
|
||||
- `ArchitectureModules`: 管理模块安装、服务模块接入和扩展点注册。
|
||||
|
||||
这组拆分的目标是降低单个核心类的职责密度,同时保持对外 API 稳定。
|
||||
|
||||
### Context Propagation
|
||||
|
||||
`IArchitectureContext` 和相关 Provider 类型负责在组件之间传播上下文能力,使 Model、System
|
||||
和外部扩展都能通过统一入口访问架构服务,而不直接耦合具体实现细节。
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### CQRS
|
||||
|
||||
命令与查询分离,支持同步与异步执行。Mediator 模式通过源码生成器集成,以减少模板代码并保持调用路径清晰。
|
||||
|
||||
### EventBus
|
||||
|
||||
类型安全事件总线支持事件发布、订阅、优先级、过滤器和弱引用订阅。它是模块之间松耦合通信的核心基础设施之一。
|
||||
|
||||
### BindableProperty
|
||||
|
||||
响应式属性模型通过值变化通知驱动界面或业务层更新,适合表达轻量级状态同步。
|
||||
|
||||
### Coroutine
|
||||
|
||||
帧驱动协程系统基于 `IYieldInstruction` 和调度器抽象,支持等待时间、事件和任务完成等常见模式。
|
||||
|
||||
### IoC
|
||||
|
||||
依赖注入通过 `MicrosoftDiContainer` 对 `Microsoft.Extensions.DependencyInjection` 进行封装,用于统一组件注册和服务解析体验。
|
||||
|
||||
### Service Modules
|
||||
|
||||
`IServiceModule` 模式用于向 Architecture 注册内置服务,例如 EventBus、CommandExecutor、QueryExecutor 等。这一模式承担“基础设施能力装配”的职责。
|
||||
|
||||
## Source Generators
|
||||
|
||||
当前仓库包含多类 Roslyn 增量源码生成器:
|
||||
|
||||
- `LoggerGenerator` (`[Log]`): 自动生成日志字段和日志辅助方法。
|
||||
- `PriorityGenerator` (`[Priority]`): 生成优先级比较相关实现。
|
||||
- `EnumExtensionsGenerator` (`[GenerateEnumExtensions]`): 生成枚举扩展能力。
|
||||
- `ContextAwareGenerator` (`[ContextAware]`): 自动实现 `IContextAware` 相关样板逻辑。
|
||||
|
||||
这些生成器的目标是减少重复代码,同时保持框架层 API 的一致性与可维护性。
|
||||
|
||||
## Module Structure
|
||||
|
||||
仓库以“抽象层 + 实现层 + 集成层 + 生成器层”的方式组织:
|
||||
|
||||
- `GFramework.Core.Abstractions` / `GFramework.Game.Abstractions`: 约束接口和公共契约。
|
||||
- `GFramework.Core` / `GFramework.Game`: 提供平台无关实现。
|
||||
- `GFramework.Godot`: 提供与 Godot 运行时集成的适配实现。
|
||||
- `GFramework.Ecs.Arch`: 提供 ECS Architecture 相关扩展。
|
||||
- `GFramework.SourceGenerators` 及相关 Abstractions/Common: 提供代码生成能力。
|
||||
|
||||
这种结构的核心设计目标是让抽象稳定、实现可替换、引擎集成隔离、生成器能力可独立演进。
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
项目文档位于 `docs/`,中文内容位于 `docs/zh-CN/`。文档内容覆盖:
|
||||
|
||||
- 入门与安装
|
||||
- Core / Game / Godot / ECS 各模块能力
|
||||
- Source Generator 使用说明
|
||||
- 教程、最佳实践与故障排查
|
||||
|
||||
阅读顺序通常建议先看根目录 `README.md` 和各子模块 `README.md`,再进入 `docs/` 查阅专题说明。
|
||||
|
||||
## Design Intent
|
||||
|
||||
GFramework 的设计重点不是把所有能力堆进单一核心类,而是通过清晰的模块边界、可组合的服务注册方式、稳定的抽象契约以及适度自动化的源码生成,构建一个适合长期演进的游戏开发基础框架。
|
||||
43
GFramework.Core.Abstractions/Concurrency/LockInfo.cs
Normal file
43
GFramework.Core.Abstractions/Concurrency/LockInfo.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2025 GeWuYou
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
namespace GFramework.Core.Abstractions.Concurrency;
|
||||
|
||||
/// <summary>
|
||||
/// 锁信息(用于调试)
|
||||
/// </summary>
|
||||
public readonly struct LockInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 锁的键。
|
||||
/// </summary>
|
||||
public string Key { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前引用计数。
|
||||
/// </summary>
|
||||
public int ReferenceCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后访问时间戳(Environment.TickCount64)。
|
||||
/// </summary>
|
||||
public long LastAccessTicks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 等待队列长度(近似值)。
|
||||
/// 注意:这是一个基于 SemaphoreSlim.CurrentCount 的近似指示器,
|
||||
/// 当 CurrentCount == 0 时表示锁被持有且可能有等待者,返回 1;
|
||||
/// 否则返回 0。这不是精确的等待者数量,仅用于调试参考。
|
||||
/// </summary>
|
||||
public int WaitingCount { get; init; }
|
||||
}
|
||||
@ -11,11 +11,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace GFramework.Core.Abstractions.Concurrency;
|
||||
|
||||
/// <summary>
|
||||
/// 锁统计信息
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly struct LockStatistics
|
||||
{
|
||||
/// <summary>
|
||||
@ -37,33 +40,4 @@ public readonly struct LockStatistics
|
||||
/// 累计清理的锁数量
|
||||
/// </summary>
|
||||
public int TotalCleaned { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 锁信息(用于调试)
|
||||
/// </summary>
|
||||
public readonly struct LockInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 锁的键
|
||||
/// </summary>
|
||||
public string Key { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前引用计数
|
||||
/// </summary>
|
||||
public int ReferenceCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后访问时间戳(Environment.TickCount64)
|
||||
/// </summary>
|
||||
public long LastAccessTicks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 等待队列长度(近似值)
|
||||
/// 注意:这是一个基于 SemaphoreSlim.CurrentCount 的近似指示器,
|
||||
/// 当 CurrentCount == 0 时表示锁被持有且可能有等待者,返回 1;
|
||||
/// 否则返回 0。这不是精确的等待者数量,仅用于调试参考。
|
||||
/// </summary>
|
||||
public int WaitingCount { get; init; }
|
||||
}
|
||||
@ -17,7 +17,7 @@
|
||||
<Using Include="GFramework.Core.Abstractions"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.19">
|
||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.25">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
namespace GFramework.Core.Abstractions.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化格式化器接口
|
||||
/// </summary>
|
||||
public interface ILocalizationFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// 格式化器名称
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 尝试格式化值
|
||||
/// </summary>
|
||||
/// <param name="format">格式字符串</param>
|
||||
/// <param name="value">要格式化的值</param>
|
||||
/// <param name="provider">格式提供者</param>
|
||||
/// <param name="result">格式化结果</param>
|
||||
/// <returns>是否成功格式化</returns>
|
||||
bool TryFormat(string format, object value, IFormatProvider? provider, out string result);
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
using System.Globalization;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
|
||||
namespace GFramework.Core.Abstractions.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化管理器接口
|
||||
/// </summary>
|
||||
public interface ILocalizationManager : ISystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前语言代码
|
||||
/// </summary>
|
||||
string CurrentLanguage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前文化信息
|
||||
/// </summary>
|
||||
CultureInfo CurrentCulture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 可用语言列表
|
||||
/// </summary>
|
||||
IReadOnlyList<string> AvailableLanguages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置当前语言
|
||||
/// </summary>
|
||||
/// <param name="languageCode">语言代码</param>
|
||||
void SetLanguage(string languageCode);
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化表
|
||||
/// </summary>
|
||||
/// <param name="tableName">表名</param>
|
||||
/// <returns>本地化表</returns>
|
||||
ILocalizationTable GetTable(string tableName);
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化文本
|
||||
/// </summary>
|
||||
/// <param name="table">表名</param>
|
||||
/// <param name="key">键名</param>
|
||||
/// <returns>本地化文本</returns>
|
||||
string GetText(string table, string key);
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串(支持变量和格式化)
|
||||
/// </summary>
|
||||
/// <param name="table">表名</param>
|
||||
/// <param name="key">键名</param>
|
||||
/// <returns>本地化字符串</returns>
|
||||
ILocalizationString GetString(string table, string key);
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取本地化文本
|
||||
/// </summary>
|
||||
/// <param name="table">表名</param>
|
||||
/// <param name="key">键名</param>
|
||||
/// <param name="text">输出文本</param>
|
||||
/// <returns>是否成功获取</returns>
|
||||
bool TryGetText(string table, string key, out string text);
|
||||
|
||||
/// <summary>
|
||||
/// 注册格式化器
|
||||
/// </summary>
|
||||
/// <param name="name">格式化器名称</param>
|
||||
/// <param name="formatter">格式化器实例</param>
|
||||
void RegisterFormatter(string name, ILocalizationFormatter formatter);
|
||||
|
||||
/// <summary>
|
||||
/// 获取格式化器
|
||||
/// </summary>
|
||||
/// <param name="name">格式化器名称</param>
|
||||
/// <returns>格式化器实例,如果不存在则返回 null</returns>
|
||||
ILocalizationFormatter? GetFormatter(string name);
|
||||
|
||||
/// <summary>
|
||||
/// 订阅语言变化事件
|
||||
/// </summary>
|
||||
/// <param name="callback">回调函数</param>
|
||||
void SubscribeToLanguageChange(Action<string> callback);
|
||||
|
||||
/// <summary>
|
||||
/// 取消订阅语言变化事件
|
||||
/// </summary>
|
||||
/// <param name="callback">回调函数</param>
|
||||
void UnsubscribeFromLanguageChange(Action<string> callback);
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
namespace GFramework.Core.Abstractions.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化字符串接口(支持变量和格式化)
|
||||
/// </summary>
|
||||
public interface ILocalizationString
|
||||
{
|
||||
/// <summary>
|
||||
/// 表名
|
||||
/// </summary>
|
||||
string Table { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 键名
|
||||
/// </summary>
|
||||
string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 添加变量
|
||||
/// </summary>
|
||||
/// <param name="name">变量名</param>
|
||||
/// <param name="value">变量值</param>
|
||||
/// <returns>当前实例(支持链式调用)</returns>
|
||||
ILocalizationString WithVariable(string name, object value);
|
||||
|
||||
/// <summary>
|
||||
/// 批量添加变量
|
||||
/// </summary>
|
||||
/// <param name="variables">变量数组</param>
|
||||
/// <returns>当前实例(支持链式调用)</returns>
|
||||
ILocalizationString WithVariables(params (string name, object value)[] variables);
|
||||
|
||||
/// <summary>
|
||||
/// 格式化并返回最终文本
|
||||
/// </summary>
|
||||
/// <returns>格式化后的文本</returns>
|
||||
string Format();
|
||||
|
||||
/// <summary>
|
||||
/// 获取原始文本(不进行格式化)
|
||||
/// </summary>
|
||||
/// <returns>原始文本</returns>
|
||||
string GetRaw();
|
||||
|
||||
/// <summary>
|
||||
/// 检查键是否存在
|
||||
/// </summary>
|
||||
/// <returns>是否存在</returns>
|
||||
bool Exists();
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
namespace GFramework.Core.Abstractions.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化表接口
|
||||
/// </summary>
|
||||
public interface ILocalizationTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 表名
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言代码
|
||||
/// </summary>
|
||||
string Language { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 回退表(当前表中找不到键时使用)
|
||||
/// </summary>
|
||||
ILocalizationTable? Fallback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取原始文本(不进行格式化)
|
||||
/// </summary>
|
||||
/// <param name="key">键名</param>
|
||||
/// <returns>原始文本</returns>
|
||||
string GetRawText(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否包含指定键
|
||||
/// </summary>
|
||||
/// <param name="key">键名</param>
|
||||
/// <returns>是否包含</returns>
|
||||
bool ContainsKey(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有键
|
||||
/// </summary>
|
||||
/// <returns>键集合</returns>
|
||||
IEnumerable<string> GetKeys();
|
||||
|
||||
/// <summary>
|
||||
/// 合并覆盖数据
|
||||
/// </summary>
|
||||
/// <param name="overrides">覆盖数据</param>
|
||||
void Merge(IReadOnlyDictionary<string, string> overrides);
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
namespace GFramework.Core.Abstractions.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化配置
|
||||
/// </summary>
|
||||
public class LocalizationConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认语言代码
|
||||
/// </summary>
|
||||
public string DefaultLanguage { get; set; } = "eng";
|
||||
|
||||
/// <summary>
|
||||
/// 回退语言代码(当目标语言缺少键时使用)
|
||||
/// </summary>
|
||||
public string FallbackLanguage { get; set; } = "eng";
|
||||
|
||||
/// <summary>
|
||||
/// 本地化文件路径(Godot 资源路径)
|
||||
/// </summary>
|
||||
public string LocalizationPath { get; set; } = "res://localization";
|
||||
|
||||
/// <summary>
|
||||
/// 用户覆盖文件路径(用于热更新和自定义翻译)
|
||||
/// </summary>
|
||||
public string OverridePath { get; set; } = "user://localization_override";
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用热重载(监视覆盖文件变化)
|
||||
/// </summary>
|
||||
public bool EnableHotReload { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否在加载时验证本地化文件
|
||||
/// </summary>
|
||||
public bool ValidateOnLoad { get; set; } = true;
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
namespace GFramework.Core.Abstractions.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化异常基类
|
||||
/// </summary>
|
||||
public class LocalizationException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化本地化异常
|
||||
/// </summary>
|
||||
public LocalizationException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化本地化异常
|
||||
/// </summary>
|
||||
/// <param name="message">异常消息</param>
|
||||
public LocalizationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化本地化异常
|
||||
/// </summary>
|
||||
/// <param name="message">异常消息</param>
|
||||
/// <param name="innerException">内部异常</param>
|
||||
public LocalizationException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
namespace GFramework.Core.Abstractions.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化键未找到异常
|
||||
/// </summary>
|
||||
public class LocalizationKeyNotFoundException : LocalizationException
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化键未找到异常
|
||||
/// </summary>
|
||||
/// <param name="tableName">表名</param>
|
||||
/// <param name="key">键名</param>
|
||||
public LocalizationKeyNotFoundException(string tableName, string key)
|
||||
: base($"Localization key '{key}' not found in table '{tableName}'")
|
||||
{
|
||||
TableName = tableName;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表名
|
||||
/// </summary>
|
||||
public string TableName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 键名
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
namespace GFramework.Core.Abstractions.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化表未找到异常
|
||||
/// </summary>
|
||||
public class LocalizationTableNotFoundException : LocalizationException
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化表未找到异常
|
||||
/// </summary>
|
||||
/// <param name="tableName">表名</param>
|
||||
public LocalizationTableNotFoundException(string tableName)
|
||||
: base($"Localization table '{tableName}' not found")
|
||||
{
|
||||
TableName = tableName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表名
|
||||
/// </summary>
|
||||
public string TableName { get; }
|
||||
}
|
||||
@ -75,7 +75,9 @@ public interface IPauseStackManager : IContextUtility
|
||||
void UnregisterHandler(IPauseHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// 暂停状态变化事件
|
||||
/// 暂停状态变化事件。
|
||||
/// 事件遵循标准 .NET 事件模式,事件源为触发通知的暂停管理器实例,
|
||||
/// 事件数据由 <see cref="PauseStateChangedEventArgs"/> 提供。
|
||||
/// </summary>
|
||||
event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||
event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
namespace GFramework.Core.Abstractions.Pause;
|
||||
|
||||
/// <summary>
|
||||
/// 表示暂停状态变化事件的数据。
|
||||
/// 该类型用于向事件订阅者传递暂停组以及该组变化后的暂停状态。
|
||||
/// </summary>
|
||||
public sealed class PauseStateChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化 <see cref="PauseStateChangedEventArgs"/> 的新实例。
|
||||
/// </summary>
|
||||
/// <param name="group">发生状态变化的暂停组。</param>
|
||||
/// <param name="isPaused">暂停组变化后的新状态。</param>
|
||||
public PauseStateChangedEventArgs(PauseGroup group, bool isPaused)
|
||||
{
|
||||
Group = group;
|
||||
IsPaused = isPaused;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取发生状态变化的暂停组。
|
||||
/// </summary>
|
||||
public PauseGroup Group { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取暂停组变化后的新状态。
|
||||
/// 为 <see langword="true"/> 表示进入暂停,为 <see langword="false"/> 表示恢复运行。
|
||||
/// </summary>
|
||||
public bool IsPaused { get; }
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
namespace GFramework.Core.Abstractions.Utility.Numeric;
|
||||
|
||||
/// <summary>
|
||||
/// 数值显示格式化器接口。
|
||||
/// </summary>
|
||||
public interface INumericDisplayFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将数值格式化为展示字符串。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数值类型。</typeparam>
|
||||
/// <param name="value">待格式化的值。</param>
|
||||
/// <param name="options">格式化选项。</param>
|
||||
/// <returns>格式化后的字符串。</returns>
|
||||
string Format<T>(T value, NumericFormatOptions? options = null);
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
namespace GFramework.Core.Abstractions.Utility.Numeric;
|
||||
|
||||
/// <summary>
|
||||
/// 数值显示规则接口。
|
||||
/// </summary>
|
||||
public interface INumericFormatRule
|
||||
{
|
||||
/// <summary>
|
||||
/// 规则名称。
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 尝试按当前规则格式化数值。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数值类型。</typeparam>
|
||||
/// <param name="value">待格式化的值。</param>
|
||||
/// <param name="options">格式化选项。</param>
|
||||
/// <param name="result">输出结果。</param>
|
||||
/// <returns>格式化是否成功。</returns>
|
||||
bool TryFormat<T>(T value, NumericFormatOptions options, out string result);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
namespace GFramework.Core.Abstractions.Utility.Numeric;
|
||||
|
||||
/// <summary>
|
||||
/// 数值显示风格。
|
||||
/// </summary>
|
||||
public enum NumericDisplayStyle
|
||||
{
|
||||
/// <summary>
|
||||
/// 紧凑缩写风格,例如 1.2K / 3.4M。
|
||||
/// </summary>
|
||||
Compact = 0
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
namespace GFramework.Core.Abstractions.Utility.Numeric;
|
||||
|
||||
/// <summary>
|
||||
/// 数值格式化选项。
|
||||
/// </summary>
|
||||
public sealed record NumericFormatOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示风格。
|
||||
/// </summary>
|
||||
public NumericDisplayStyle Style { get; init; } = NumericDisplayStyle.Compact;
|
||||
|
||||
/// <summary>
|
||||
/// 最大保留小数位数。
|
||||
/// </summary>
|
||||
public int MaxDecimalPlaces { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 最少保留小数位数。
|
||||
/// </summary>
|
||||
public int MinDecimalPlaces { get; init; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 四舍五入策略。
|
||||
/// </summary>
|
||||
public MidpointRounding MidpointRounding { get; init; } = MidpointRounding.AwayFromZero;
|
||||
|
||||
/// <summary>
|
||||
/// 是否裁剪小数末尾的 0。
|
||||
/// </summary>
|
||||
public bool TrimTrailingZeros { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 小于缩写阈值时是否启用千分位分组。
|
||||
/// </summary>
|
||||
public bool UseGroupingBelowThreshold { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 进入缩写显示的阈值。
|
||||
/// </summary>
|
||||
public decimal CompactThreshold { get; init; } = 1000m;
|
||||
|
||||
/// <summary>
|
||||
/// 格式提供者。
|
||||
/// </summary>
|
||||
public IFormatProvider? FormatProvider { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义格式规则。
|
||||
/// </summary>
|
||||
public INumericFormatRule? Rule { get; init; }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace GFramework.Core.Abstractions.Utility.Numeric;
|
||||
|
||||
/// <summary>
|
||||
/// 数值缩写阈值定义。
|
||||
/// </summary>
|
||||
/// <param name="Divisor">缩写除数,例如 1000、1000000。</param>
|
||||
/// <param name="Suffix">缩写后缀,例如 K、M。</param>
|
||||
public readonly record struct NumericSuffixThreshold(decimal Divisor, string Suffix);
|
||||
@ -41,16 +41,8 @@ public abstract class TestArchitectureBase : Architecture
|
||||
{
|
||||
InitCalled = true;
|
||||
_postRegistrationHook?.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进入指定架构阶段时的处理方法,记录阶段历史
|
||||
/// </summary>
|
||||
/// <param name="next">要进入的下一个架构阶段</param>
|
||||
protected override void EnterPhase(ArchitecturePhase next)
|
||||
{
|
||||
base.EnterPhase(next);
|
||||
// 记录进入的架构阶段到历史列表中
|
||||
PhaseHistory.Add(next);
|
||||
// 订阅阶段变更事件以记录历史
|
||||
PhaseChanged += phase => PhaseHistory.Add(phase);
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
|
||||
<TestTargetFrameworks Condition="'$(TestTargetFrameworks)' == ''">net10.0</TestTargetFrameworks>
|
||||
<TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<WarningLevel>0</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Mediator.Abstractions" Version="3.0.1"/>
|
||||
@ -20,6 +22,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Scriban" Version="6.6.0" />
|
||||
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
||||
<ProjectReference Include="..\GFramework.SourceGenerators.Abstractions\GFramework.SourceGenerators.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\GFramework.SourceGenerators.Common\GFramework.SourceGenerators.Common.csproj"/>
|
||||
|
||||
@ -0,0 +1,190 @@
|
||||
using System.IO;
|
||||
using GFramework.Core.Abstractions.Localization;
|
||||
using GFramework.Core.Localization;
|
||||
|
||||
namespace GFramework.Core.Tests.Localization;
|
||||
|
||||
[TestFixture]
|
||||
public class LocalizationIntegrationTests
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_testDataPath = Path.Combine(Path.GetTempPath(), $"gframework_localization_{Guid.NewGuid():N}");
|
||||
CreateTestLocalizationFiles(_testDataPath);
|
||||
|
||||
var config = new LocalizationConfig
|
||||
{
|
||||
DefaultLanguage = "eng",
|
||||
FallbackLanguage = "eng",
|
||||
LocalizationPath = _testDataPath,
|
||||
EnableHotReload = false,
|
||||
ValidateOnLoad = false
|
||||
};
|
||||
|
||||
_manager = new LocalizationManager(config);
|
||||
_manager.Initialize();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (Directory.Exists(_testDataPath))
|
||||
{
|
||||
Directory.Delete(_testDataPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private LocalizationManager? _manager;
|
||||
private string _testDataPath = null!;
|
||||
|
||||
private static void CreateTestLocalizationFiles(string rootPath)
|
||||
{
|
||||
var engPath = Path.Combine(rootPath, "eng");
|
||||
var zhsPath = Path.Combine(rootPath, "zhs");
|
||||
Directory.CreateDirectory(engPath);
|
||||
Directory.CreateDirectory(zhsPath);
|
||||
|
||||
File.WriteAllText(Path.Combine(engPath, "common.json"), """
|
||||
{
|
||||
"game.title": "My Game",
|
||||
"ui.message.welcome": "Welcome, {playerName}!",
|
||||
"status.health": "Health: {current}/{max}",
|
||||
"status.gold": "Gold: {gold:compact}",
|
||||
"status.damage": "Damage: {damage:compact:maxDecimals=2}",
|
||||
"status.unknownCompact": "Gold: {gold:compact:maxDecimalss=2}",
|
||||
"status.invalidCompact": "Gold: {gold:compact:maxDecimals=abc}"
|
||||
}
|
||||
""");
|
||||
|
||||
File.WriteAllText(Path.Combine(zhsPath, "common.json"), """
|
||||
{
|
||||
"game.title": "我的游戏",
|
||||
"ui.message.welcome": "欢迎, {playerName}!",
|
||||
"status.health": "生命值: {current}/{max}",
|
||||
"status.gold": "金币: {gold:compact}",
|
||||
"status.damage": "伤害: {damage:compact:maxDecimals=2}",
|
||||
"status.unknownCompact": "金币: {gold:compact:maxDecimalss=2}",
|
||||
"status.invalidCompact": "金币: {gold:compact:maxDecimals=abc}"
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetText_ShouldReturnEnglishText()
|
||||
{
|
||||
// Act
|
||||
var title = _manager!.GetText("common", "game.title");
|
||||
|
||||
// Assert
|
||||
Assert.That(title, Is.EqualTo("My Game"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetString_WithVariable_ShouldFormatCorrectly()
|
||||
{
|
||||
// Act
|
||||
var message = _manager!.GetString("common", "ui.message.welcome")
|
||||
.WithVariable("playerName", "Alice")
|
||||
.Format();
|
||||
|
||||
// Assert
|
||||
Assert.That(message, Is.EqualTo("Welcome, Alice!"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetLanguage_ShouldSwitchToChineseText()
|
||||
{
|
||||
// Act
|
||||
_manager!.SetLanguage("zhs");
|
||||
var title = _manager.GetText("common", "game.title");
|
||||
|
||||
// Assert
|
||||
Assert.That(title, Is.EqualTo("我的游戏"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetString_WithMultipleVariables_ShouldFormatCorrectly()
|
||||
{
|
||||
// Act
|
||||
var health = _manager!.GetString("common", "status.health")
|
||||
.WithVariable("current", 80)
|
||||
.WithVariable("max", 100)
|
||||
.Format();
|
||||
|
||||
// Assert
|
||||
Assert.That(health, Is.EqualTo("Health: 80/100"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetString_WithCompactFormatter_ShouldFormatCorrectly()
|
||||
{
|
||||
var gold = _manager!.GetString("common", "status.gold")
|
||||
.WithVariable("gold", 1_250)
|
||||
.Format();
|
||||
|
||||
Assert.That(gold, Is.EqualTo("Gold: 1.3K"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetString_WithCompactFormatterArgs_ShouldApplyOptions()
|
||||
{
|
||||
var damage = _manager!.GetString("common", "status.damage")
|
||||
.WithVariable("damage", 1_234)
|
||||
.Format();
|
||||
|
||||
Assert.That(damage, Is.EqualTo("Damage: 1.23K"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetString_WithUnknownCompactFormatterArgs_ShouldIgnoreUnknownOptions()
|
||||
{
|
||||
var gold = _manager!.GetString("common", "status.unknownCompact")
|
||||
.WithVariable("gold", 1_250)
|
||||
.Format();
|
||||
|
||||
Assert.That(gold, Is.EqualTo("Gold: 1.3K"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetString_WithInvalidCompactFormatterArgs_ShouldFallbackToDefaultFormatting()
|
||||
{
|
||||
var gold = _manager!.GetString("common", "status.invalidCompact")
|
||||
.WithVariable("gold", 1_250)
|
||||
.Format();
|
||||
|
||||
Assert.That(gold, Is.EqualTo("Gold: 1250"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LanguageChange_ShouldTriggerCallback()
|
||||
{
|
||||
// Arrange
|
||||
var callbackTriggered = false;
|
||||
var newLanguage = string.Empty;
|
||||
|
||||
_manager!.SubscribeToLanguageChange(lang =>
|
||||
{
|
||||
callbackTriggered = true;
|
||||
newLanguage = lang;
|
||||
});
|
||||
|
||||
// Act
|
||||
_manager.SetLanguage("zhs");
|
||||
|
||||
// Assert
|
||||
Assert.That(callbackTriggered, Is.True);
|
||||
Assert.That(newLanguage, Is.EqualTo("zhs"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AvailableLanguages_ShouldContainBothLanguages()
|
||||
{
|
||||
// Act
|
||||
var languages = _manager!.AvailableLanguages;
|
||||
|
||||
// Assert
|
||||
Assert.That(languages, Contains.Item("eng"));
|
||||
Assert.That(languages, Contains.Item("zhs"));
|
||||
}
|
||||
}
|
||||
84
GFramework.Core.Tests/Localization/LocalizationTableTests.cs
Normal file
84
GFramework.Core.Tests/Localization/LocalizationTableTests.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using GFramework.Core.Localization;
|
||||
|
||||
namespace GFramework.Core.Tests.Localization;
|
||||
|
||||
[TestFixture]
|
||||
public class LocalizationTableTests
|
||||
{
|
||||
[Test]
|
||||
public void GetRawText_ShouldReturnCorrectText()
|
||||
{
|
||||
// Arrange
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
["test.key"] = "Test Value"
|
||||
};
|
||||
var table = new LocalizationTable("test", "eng", data);
|
||||
|
||||
// Act
|
||||
var result = table.GetRawText("test.key");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo("Test Value"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRawText_WithFallback_ShouldReturnFallbackValue()
|
||||
{
|
||||
// Arrange
|
||||
var fallbackData = new Dictionary<string, string>
|
||||
{
|
||||
["test.key"] = "Fallback Value"
|
||||
};
|
||||
var fallbackTable = new LocalizationTable("test", "eng", fallbackData);
|
||||
|
||||
var data = new Dictionary<string, string>();
|
||||
var table = new LocalizationTable("test", "zhs", data, fallbackTable);
|
||||
|
||||
// Act
|
||||
var result = table.GetRawText("test.key");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo("Fallback Value"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ContainsKey_ShouldReturnTrue_WhenKeyExists()
|
||||
{
|
||||
// Arrange
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
["test.key"] = "Test Value"
|
||||
};
|
||||
var table = new LocalizationTable("test", "eng", data);
|
||||
|
||||
// Act
|
||||
var result = table.ContainsKey("test.key");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_ShouldOverrideExistingValues()
|
||||
{
|
||||
// Arrange
|
||||
var data = new Dictionary<string, string>
|
||||
{
|
||||
["test.key"] = "Original Value"
|
||||
};
|
||||
var table = new LocalizationTable("test", "eng", data);
|
||||
|
||||
var overrides = new Dictionary<string, string>
|
||||
{
|
||||
["test.key"] = "Override Value"
|
||||
};
|
||||
|
||||
// Act
|
||||
table.Merge(overrides);
|
||||
var result = table.GetRawText("test.key");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo("Override Value"));
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Logging.Appenders;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GFramework.Core.Tests.Logging;
|
||||
|
||||
@ -152,8 +151,12 @@ public class AsyncLogAppenderTests
|
||||
[Test]
|
||||
public void Append_WhenInnerAppenderThrows_ShouldNotCrash()
|
||||
{
|
||||
var reportedExceptions = new List<Exception>();
|
||||
var innerAppender = new ThrowingAppender();
|
||||
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000);
|
||||
using var asyncAppender = new AsyncLogAppender(
|
||||
innerAppender,
|
||||
bufferSize: 1000,
|
||||
processingErrorHandler: reportedExceptions.Add);
|
||||
|
||||
// 即使内部 Appender 抛出异常,也不应该影响调用线程
|
||||
Assert.DoesNotThrow(() =>
|
||||
@ -165,7 +168,56 @@ public class AsyncLogAppenderTests
|
||||
}
|
||||
});
|
||||
|
||||
Thread.Sleep(100); // 等待后台处理
|
||||
asyncAppender.Flush();
|
||||
|
||||
Assert.That(reportedExceptions, Has.Count.EqualTo(10));
|
||||
Assert.That(reportedExceptions, Has.All.TypeOf<InvalidOperationException>());
|
||||
Assert.That(reportedExceptions.Select(static exception => exception.Message),
|
||||
Has.All.EqualTo("Test exception"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Append_WhenProcessingErrorHandlerThrows_ShouldStillNotCrash()
|
||||
{
|
||||
var innerAppender = new ThrowingAppender();
|
||||
using var asyncAppender = new AsyncLogAppender(
|
||||
innerAppender,
|
||||
bufferSize: 1000,
|
||||
processingErrorHandler: static _ => throw new InvalidOperationException("Observer failure"));
|
||||
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
|
||||
asyncAppender.Append(entry);
|
||||
}
|
||||
});
|
||||
|
||||
Assert.That(asyncAppender.Flush(), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Append_WhenInnerAppenderThrowsOperationCanceledException_ShouldNotReportError()
|
||||
{
|
||||
var reportedExceptions = new List<Exception>();
|
||||
var innerAppender = new CancellationAppender();
|
||||
using var asyncAppender = new AsyncLogAppender(
|
||||
innerAppender,
|
||||
bufferSize: 1000,
|
||||
processingErrorHandler: reportedExceptions.Add);
|
||||
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
|
||||
asyncAppender.Append(entry);
|
||||
}
|
||||
});
|
||||
|
||||
Assert.That(asyncAppender.Flush(), Is.True);
|
||||
Assert.That(reportedExceptions, Is.Empty);
|
||||
}
|
||||
|
||||
// 辅助测试类
|
||||
@ -228,4 +280,20 @@ public class AsyncLogAppenderTests
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private class CancellationAppender : ILogAppender
|
||||
{
|
||||
public void Append(LogEntry entry)
|
||||
{
|
||||
throw new OperationCanceledException("Simulated cancellation");
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
using GFramework.Core.Abstractions.Pause;
|
||||
using GFramework.Core.Pause;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GFramework.Core.Tests.Pause;
|
||||
|
||||
@ -220,11 +219,11 @@ public class PauseStackManagerTests
|
||||
PauseGroup? eventGroup = null;
|
||||
bool? eventIsPaused = null;
|
||||
|
||||
_manager.OnPauseStateChanged += (group, isPaused) =>
|
||||
_manager.OnPauseStateChanged += (_, e) =>
|
||||
{
|
||||
eventTriggered = true;
|
||||
eventGroup = group;
|
||||
eventIsPaused = isPaused;
|
||||
eventGroup = e.Group;
|
||||
eventIsPaused = e.IsPaused;
|
||||
};
|
||||
|
||||
_manager.Push("Test", PauseGroup.Gameplay);
|
||||
@ -243,10 +242,10 @@ public class PauseStackManagerTests
|
||||
var token = _manager.Push("Test");
|
||||
|
||||
bool eventTriggered = false;
|
||||
_manager.OnPauseStateChanged += (group, isPaused) =>
|
||||
_manager.OnPauseStateChanged += (_, e) =>
|
||||
{
|
||||
eventTriggered = true;
|
||||
Assert.That(isPaused, Is.False);
|
||||
Assert.That(e.IsPaused, Is.False);
|
||||
};
|
||||
|
||||
_manager.Pop(token);
|
||||
|
||||
129
GFramework.Core.Tests/Utility/NumericDisplayFormatterTests.cs
Normal file
129
GFramework.Core.Tests/Utility/NumericDisplayFormatterTests.cs
Normal file
@ -0,0 +1,129 @@
|
||||
using System.Globalization;
|
||||
using GFramework.Core.Abstractions.Utility.Numeric;
|
||||
using GFramework.Core.Extensions;
|
||||
using GFramework.Core.Utility.Numeric;
|
||||
|
||||
namespace GFramework.Core.Tests.Utility;
|
||||
|
||||
[TestFixture]
|
||||
public class NumericDisplayFormatterTests
|
||||
{
|
||||
[Test]
|
||||
public void FormatCompact_ShouldReturnPlainText_WhenValueIsBelowThreshold()
|
||||
{
|
||||
var result = NumericDisplay.FormatCompact(950);
|
||||
|
||||
Assert.That(result, Is.EqualTo("950"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatCompact_ShouldFormatInt_AsCompactText()
|
||||
{
|
||||
var result = NumericDisplay.FormatCompact(1_200);
|
||||
|
||||
Assert.That(result, Is.EqualTo("1.2K"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatCompact_ShouldFormatLong_AsCompactText()
|
||||
{
|
||||
var result = NumericDisplay.FormatCompact(1_000_000L);
|
||||
|
||||
Assert.That(result, Is.EqualTo("1M"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatCompact_ShouldFormatDecimal_AsCompactText()
|
||||
{
|
||||
var result = NumericDisplay.FormatCompact(1_234.56m);
|
||||
|
||||
Assert.That(result, Is.EqualTo("1.2K"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatCompact_ShouldFormatNegativeValues()
|
||||
{
|
||||
var result = NumericDisplay.FormatCompact(-1_250);
|
||||
|
||||
Assert.That(result, Is.EqualTo("-1.3K"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatCompact_ShouldPromoteRoundedBoundary_ToNextSuffix()
|
||||
{
|
||||
var result = NumericDisplay.FormatCompact(999_950);
|
||||
|
||||
Assert.That(result, Is.EqualTo("1M"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Format_ShouldRespectFormatProvider()
|
||||
{
|
||||
var result = NumericDisplay.Format(1_234.5m, new NumericFormatOptions
|
||||
{
|
||||
CompactThreshold = 10_000m,
|
||||
FormatProvider = CultureInfo.GetCultureInfo("de-DE")
|
||||
});
|
||||
|
||||
Assert.That(result, Is.EqualTo("1234,5"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Format_ShouldUseGroupingBelowThreshold_WhenEnabled()
|
||||
{
|
||||
var result = NumericDisplay.Format(12_345, new NumericFormatOptions
|
||||
{
|
||||
CompactThreshold = 1_000_000m,
|
||||
UseGroupingBelowThreshold = true,
|
||||
FormatProvider = CultureInfo.InvariantCulture
|
||||
});
|
||||
|
||||
Assert.That(result, Is.EqualTo("12,345"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Format_ShouldSupportCustomSuffixRule()
|
||||
{
|
||||
var rule = new NumericSuffixFormatRule("custom",
|
||||
[
|
||||
new NumericSuffixThreshold(10m, "X"),
|
||||
new NumericSuffixThreshold(100m, "Y")
|
||||
]);
|
||||
|
||||
var result = NumericDisplay.Format(123, new NumericFormatOptions
|
||||
{
|
||||
Rule = rule,
|
||||
CompactThreshold = 10m,
|
||||
FormatProvider = CultureInfo.InvariantCulture
|
||||
});
|
||||
|
||||
Assert.That(result, Is.EqualTo("1.2Y"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Format_ShouldHandlePositiveInfinity()
|
||||
{
|
||||
var result = NumericDisplay.Format(double.PositiveInfinity, new NumericFormatOptions
|
||||
{
|
||||
FormatProvider = CultureInfo.InvariantCulture
|
||||
});
|
||||
|
||||
Assert.That(result, Is.EqualTo("Infinity"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Format_ObjectOverload_ShouldDispatchToNumericFormatter()
|
||||
{
|
||||
var result = NumericDisplay.Format((object)1_234m);
|
||||
|
||||
Assert.That(result, Is.EqualTo("1.2K"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ToCompactString_ShouldUseNumericExtension()
|
||||
{
|
||||
var result = 15_320.ToCompactString();
|
||||
|
||||
Assert.That(result, Is.EqualTo("15.3K"));
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,11 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Enums;
|
||||
using GFramework.Core.Abstractions.Environment;
|
||||
using GFramework.Core.Abstractions.Ioc;
|
||||
using GFramework.Core.Abstractions.Lifecycle;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.Environment;
|
||||
using GFramework.Core.Extensions;
|
||||
using GFramework.Core.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -17,41 +14,56 @@ namespace GFramework.Core.Architectures;
|
||||
/// <summary>
|
||||
/// 架构基类,提供系统、模型、工具等组件的注册与管理功能。
|
||||
/// 专注于生命周期管理、初始化流程控制和架构阶段转换。
|
||||
///
|
||||
/// 重构说明:此类已重构为协调器模式,将职责委托给专门的管理器:
|
||||
/// - ArchitectureLifecycle: 生命周期管理
|
||||
/// - ArchitectureComponentRegistry: 组件注册管理
|
||||
/// - ArchitectureModules: 模块管理
|
||||
/// </summary>
|
||||
public abstract class Architecture(
|
||||
IArchitectureConfiguration? configuration = null,
|
||||
IEnvironment? environment = null,
|
||||
IArchitectureServices? services = null,
|
||||
IArchitectureContext? context = null
|
||||
)
|
||||
: IArchitecture
|
||||
public abstract class Architecture : IArchitecture
|
||||
{
|
||||
#region Module Management
|
||||
#region Constructor
|
||||
|
||||
/// <summary>
|
||||
/// 注册中介行为管道
|
||||
/// 用于配置Mediator框架的行为拦截和处理逻辑
|
||||
/// 构造函数,初始化架构和管理器
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
||||
/// <param name="configuration">架构配置</param>
|
||||
/// <param name="environment">环境配置</param>
|
||||
/// <param name="services">服务管理器</param>
|
||||
/// <param name="context">架构上下文</param>
|
||||
protected Architecture(
|
||||
IArchitectureConfiguration? configuration = null,
|
||||
IEnvironment? environment = null,
|
||||
IArchitectureServices? services = null,
|
||||
IArchitectureContext? context = null)
|
||||
{
|
||||
_logger.Debug($"Registering mediator behavior: {typeof(TBehavior).Name}");
|
||||
Container.RegisterMediatorBehavior<TBehavior>();
|
||||
Configuration = configuration ?? new ArchitectureConfiguration();
|
||||
Environment = environment ?? new DefaultEnvironment();
|
||||
Services = services ?? new ArchitectureServices();
|
||||
_context = context;
|
||||
|
||||
// 初始化 Logger
|
||||
LoggerFactoryResolver.Provider = Configuration.LoggerProperties.LoggerFactoryProvider;
|
||||
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
|
||||
|
||||
// 初始化管理器
|
||||
_lifecycle = new ArchitectureLifecycle(this, Configuration, Services, _logger);
|
||||
_componentRegistry = new ArchitectureComponentRegistry(this, Configuration, Services, _lifecycle, _logger);
|
||||
_modules = new ArchitectureModules(this, Services, _logger);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle Hook Management
|
||||
|
||||
/// <summary>
|
||||
/// 安装架构模块
|
||||
/// 注册生命周期钩子
|
||||
/// </summary>
|
||||
/// <param name="module">要安装的模块</param>
|
||||
/// <returns>安装的模块实例</returns>
|
||||
public IArchitectureModule InstallModule(IArchitectureModule module)
|
||||
/// <param name="hook">生命周期钩子实例</param>
|
||||
/// <returns>注册的钩子实例</returns>
|
||||
public IArchitectureLifecycleHook RegisterLifecycleHook(IArchitectureLifecycleHook hook)
|
||||
{
|
||||
var name = module.GetType().Name;
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger(name);
|
||||
logger.Debug($"Installing module: {name}");
|
||||
module.Install(this);
|
||||
logger.Info($"Module installed: {name}");
|
||||
return module;
|
||||
return _lifecycle.RegisterLifecycleHook(hook);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -61,361 +73,99 @@ public abstract class Architecture(
|
||||
/// <summary>
|
||||
/// 获取架构配置对象
|
||||
/// </summary>
|
||||
private IArchitectureConfiguration Configuration { get; } = configuration ?? new ArchitectureConfiguration();
|
||||
private IArchitectureConfiguration Configuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取环境配置对象
|
||||
/// </summary>
|
||||
private IEnvironment Environment { get; } = environment ?? new DefaultEnvironment();
|
||||
|
||||
private IArchitectureServices Services { get; } = services ?? new ArchitectureServices();
|
||||
private IEnvironment Environment { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取依赖注入容器
|
||||
/// 获取服务管理器
|
||||
/// </summary>
|
||||
private IIocContainer Container => Services.Container;
|
||||
private IArchitectureServices Services { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前架构的阶段
|
||||
/// </summary>
|
||||
public ArchitecturePhase CurrentPhase { get; private set; }
|
||||
public ArchitecturePhase CurrentPhase => _lifecycle.CurrentPhase;
|
||||
|
||||
/// <summary>
|
||||
/// 架构上下文
|
||||
/// </summary>
|
||||
public IArchitectureContext Context => _context!;
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个布尔值,指示当前架构是否处于就绪状态
|
||||
/// </summary>
|
||||
public bool IsReady => _lifecycle.IsReady;
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于配置服务集合的委托
|
||||
/// 默认实现返回null,子类可以重写此属性以提供自定义配置逻辑
|
||||
/// </summary>
|
||||
public virtual Action<IServiceCollection>? Configurator => null;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段变更事件(用于测试和扩展)
|
||||
/// </summary>
|
||||
public event Action<ArchitecturePhase>? PhaseChanged
|
||||
{
|
||||
add => _lifecycle.PhaseChanged += value;
|
||||
remove => _lifecycle.PhaseChanged -= value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
private readonly TaskCompletionSource _readyTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个布尔值,指示当前架构是否处于就绪状态。
|
||||
/// 当前架构的阶段等于 ArchitecturePhase.Ready 时返回 true,否则返回 false。
|
||||
/// </summary>
|
||||
public bool IsReady => CurrentPhase == ArchitecturePhase.Ready;
|
||||
|
||||
/// <summary>
|
||||
/// 待初始化组件的去重集合。
|
||||
/// 用于存储需要初始化的组件实例,确保每个组件仅被初始化一次。
|
||||
/// </summary>
|
||||
private readonly HashSet<IInitializable> _pendingInitializableSet = [];
|
||||
|
||||
/// <summary>
|
||||
/// 存储所有待初始化的组件(统一管理,保持注册顺序)
|
||||
/// </summary>
|
||||
private readonly List<IInitializable> _pendingInitializableList = [];
|
||||
|
||||
/// <summary>
|
||||
/// 可销毁组件的去重集合(支持 IDestroyable 和 IAsyncDestroyable)
|
||||
/// </summary>
|
||||
private readonly HashSet<object> _disposableSet = [];
|
||||
|
||||
/// <summary>
|
||||
/// 存储所有需要销毁的组件(统一管理,保持注册逆序销毁)
|
||||
/// </summary>
|
||||
private readonly List<object> _disposables = [];
|
||||
|
||||
/// <summary>
|
||||
/// 生命周期感知对象列表
|
||||
/// </summary>
|
||||
private readonly List<IArchitectureLifecycleHook> _lifecycleHooks = [];
|
||||
|
||||
/// <summary>
|
||||
/// 标记架构是否已初始化完成
|
||||
/// </summary>
|
||||
private bool _mInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// 日志记录器实例
|
||||
/// </summary>
|
||||
private ILogger _logger = null!;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 架构上下文实例
|
||||
/// </summary>
|
||||
private IArchitectureContext? _context = context;
|
||||
private IArchitectureContext? _context;
|
||||
|
||||
/// <summary>
|
||||
/// 生命周期管理器
|
||||
/// </summary>
|
||||
private readonly ArchitectureLifecycle _lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// 组件注册管理器
|
||||
/// </summary>
|
||||
private readonly ArchitectureComponentRegistry _componentRegistry;
|
||||
|
||||
/// <summary>
|
||||
/// 模块管理器
|
||||
/// </summary>
|
||||
private readonly ArchitectureModules _modules;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle Management
|
||||
#region Module Management
|
||||
|
||||
/// <summary>
|
||||
/// 进入指定的架构阶段,并执行相应的生命周期管理操作
|
||||
/// 注册中介行为管道
|
||||
/// 用于配置Mediator框架的行为拦截和处理逻辑
|
||||
/// </summary>
|
||||
/// <param name="next">要进入的下一个架构阶段</param>
|
||||
/// <exception cref="InvalidOperationException">当阶段转换不被允许时抛出异常</exception>
|
||||
protected virtual void EnterPhase(ArchitecturePhase next)
|
||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
// 验证阶段转换
|
||||
ValidatePhaseTransition(next);
|
||||
|
||||
// 执行阶段转换
|
||||
var previousPhase = CurrentPhase;
|
||||
CurrentPhase = next;
|
||||
|
||||
if (previousPhase != next)
|
||||
_logger.Info($"Architecture phase changed: {previousPhase} -> {next}");
|
||||
|
||||
// 通知阶段变更
|
||||
NotifyPhase(next);
|
||||
NotifyPhaseAwareObjects(next);
|
||||
_modules.RegisterMediatorBehavior<TBehavior>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证阶段转换是否合法
|
||||
/// 安装架构模块
|
||||
/// </summary>
|
||||
/// <param name="next">目标阶段</param>
|
||||
/// <exception cref="InvalidOperationException">当阶段转换不合法时抛出</exception>
|
||||
private void ValidatePhaseTransition(ArchitecturePhase next)
|
||||
/// <param name="module">要安装的模块</param>
|
||||
/// <returns>安装的模块实例</returns>
|
||||
public IArchitectureModule InstallModule(IArchitectureModule module)
|
||||
{
|
||||
// 不需要严格验证,直接返回
|
||||
if (!Configuration.ArchitectureProperties.StrictPhaseValidation)
|
||||
return;
|
||||
|
||||
// FailedInitialization 可以从任何阶段转换,直接返回
|
||||
if (next == ArchitecturePhase.FailedInitialization)
|
||||
return;
|
||||
|
||||
// 检查转换是否在允许列表中
|
||||
if (ArchitectureConstants.PhaseTransitions.TryGetValue(CurrentPhase, out var allowed) &&
|
||||
allowed.Contains(next))
|
||||
return;
|
||||
|
||||
// 转换不合法,抛出异常
|
||||
var errorMsg = $"Invalid phase transition: {CurrentPhase} -> {next}";
|
||||
_logger.Fatal(errorMsg);
|
||||
throw new InvalidOperationException(errorMsg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知所有架构阶段感知对象阶段变更
|
||||
/// </summary>
|
||||
/// <param name="phase">新阶段</param>
|
||||
private void NotifyPhaseAwareObjects(ArchitecturePhase phase)
|
||||
{
|
||||
foreach (var obj in Container.GetAll<IArchitecturePhaseListener>())
|
||||
{
|
||||
_logger.Trace($"Notifying phase-aware object {obj.GetType().Name} of phase change to {phase}");
|
||||
obj.OnArchitecturePhase(phase);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知所有生命周期钩子当前阶段变更
|
||||
/// </summary>
|
||||
/// <param name="phase">当前架构阶段</param>
|
||||
private void NotifyPhase(ArchitecturePhase phase)
|
||||
{
|
||||
foreach (var hook in _lifecycleHooks)
|
||||
{
|
||||
hook.OnPhase(phase, this);
|
||||
_logger.Trace($"Notifying lifecycle hook {hook.GetType().Name} of phase {phase}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册生命周期钩子
|
||||
/// </summary>
|
||||
/// <param name="hook">生命周期钩子实例</param>
|
||||
/// <returns>注册的钩子实例</returns>
|
||||
public IArchitectureLifecycleHook RegisterLifecycleHook(IArchitectureLifecycleHook hook)
|
||||
{
|
||||
if (CurrentPhase >= ArchitecturePhase.Ready && !Configuration.ArchitectureProperties.AllowLateRegistration)
|
||||
throw new InvalidOperationException(
|
||||
"Cannot register lifecycle hook after architecture is Ready");
|
||||
_lifecycleHooks.Add(hook);
|
||||
return hook;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一的组件生命周期注册逻辑
|
||||
/// </summary>
|
||||
/// <param name="component">要注册的组件</param>
|
||||
private void RegisterLifecycleComponent<T>(T component)
|
||||
{
|
||||
// 处理初始化
|
||||
if (component is IInitializable initializable)
|
||||
{
|
||||
if (!_mInitialized)
|
||||
{
|
||||
// 原子去重:HashSet.Add 返回 true 表示添加成功(之前不存在)
|
||||
if (_pendingInitializableSet.Add(initializable))
|
||||
{
|
||||
_pendingInitializableList.Add(initializable);
|
||||
_logger.Trace($"Added {component.GetType().Name} to pending initialization queue");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cannot initialize component after Architecture is Ready");
|
||||
}
|
||||
}
|
||||
|
||||
// 处理销毁(支持 IDestroyable 或 IAsyncDestroyable)
|
||||
if (component is not (IDestroyable or IAsyncDestroyable)) return;
|
||||
// 原子去重:HashSet.Add 返回 true 表示添加成功(之前不存在)
|
||||
if (!_disposableSet.Add(component)) return;
|
||||
_disposables.Add(component);
|
||||
_logger.Trace($"Registered {component.GetType().Name} for destruction");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化所有待初始化的组件
|
||||
/// </summary>
|
||||
/// <param name="asyncMode">是否使用异步模式</param>
|
||||
private async Task InitializeAllComponentsAsync(bool asyncMode)
|
||||
{
|
||||
_logger.Info($"Initializing {_pendingInitializableList.Count} components");
|
||||
|
||||
// 按类型分组初始化(保持原有的阶段划分)
|
||||
var utilities = _pendingInitializableList.OfType<IContextUtility>().ToList();
|
||||
var models = _pendingInitializableList.OfType<IModel>().ToList();
|
||||
var systems = _pendingInitializableList.OfType<ISystem>().ToList();
|
||||
|
||||
// 1. 工具初始化阶段(始终进入阶段,仅在有组件时执行初始化)
|
||||
EnterPhase(ArchitecturePhase.BeforeUtilityInit);
|
||||
|
||||
if (utilities.Count != 0)
|
||||
{
|
||||
_logger.Info($"Initializing {utilities.Count} context utilities");
|
||||
|
||||
foreach (var utility in utilities)
|
||||
{
|
||||
_logger.Debug($"Initializing utility: {utility.GetType().Name}");
|
||||
await InitializeComponentAsync(utility, asyncMode);
|
||||
}
|
||||
|
||||
_logger.Info("All context utilities initialized");
|
||||
}
|
||||
|
||||
EnterPhase(ArchitecturePhase.AfterUtilityInit);
|
||||
|
||||
// 2. 模型初始化阶段(始终进入阶段,仅在有组件时执行初始化)
|
||||
EnterPhase(ArchitecturePhase.BeforeModelInit);
|
||||
|
||||
if (models.Count != 0)
|
||||
{
|
||||
_logger.Info($"Initializing {models.Count} models");
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
_logger.Debug($"Initializing model: {model.GetType().Name}");
|
||||
await InitializeComponentAsync(model, asyncMode);
|
||||
}
|
||||
|
||||
_logger.Info("All models initialized");
|
||||
}
|
||||
|
||||
EnterPhase(ArchitecturePhase.AfterModelInit);
|
||||
|
||||
// 3. 系统初始化阶段(始终进入阶段,仅在有组件时执行初始化)
|
||||
EnterPhase(ArchitecturePhase.BeforeSystemInit);
|
||||
|
||||
if (systems.Count != 0)
|
||||
{
|
||||
_logger.Info($"Initializing {systems.Count} systems");
|
||||
|
||||
foreach (var system in systems)
|
||||
{
|
||||
_logger.Debug($"Initializing system: {system.GetType().Name}");
|
||||
await InitializeComponentAsync(system, asyncMode);
|
||||
}
|
||||
|
||||
_logger.Info("All systems initialized");
|
||||
}
|
||||
|
||||
EnterPhase(ArchitecturePhase.AfterSystemInit);
|
||||
|
||||
_pendingInitializableList.Clear();
|
||||
_pendingInitializableSet.Clear();
|
||||
_logger.Info("All components initialized");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步初始化单个组件
|
||||
/// </summary>
|
||||
/// <param name="component">要初始化的组件</param>
|
||||
/// <param name="asyncMode">是否使用异步模式</param>
|
||||
private static async Task InitializeComponentAsync(IInitializable component, bool asyncMode)
|
||||
{
|
||||
if (asyncMode && component is IAsyncInitializable asyncInit)
|
||||
await asyncInit.InitializeAsync();
|
||||
else
|
||||
component.Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 抽象初始化方法,由子类重写以进行自定义初始化操作
|
||||
/// </summary>
|
||||
protected abstract void OnInitialize();
|
||||
|
||||
/// <summary>
|
||||
/// 异步销毁架构及所有组件
|
||||
/// </summary>
|
||||
public virtual async ValueTask DestroyAsync()
|
||||
{
|
||||
// 检查当前阶段,如果已经处于销毁或已销毁状态则直接返回
|
||||
if (CurrentPhase >= ArchitecturePhase.Destroying)
|
||||
{
|
||||
_logger.Warn("Architecture destroy called but already in destroying/destroyed state");
|
||||
return;
|
||||
}
|
||||
|
||||
// 进入销毁阶段
|
||||
_logger.Info("Starting architecture destruction");
|
||||
EnterPhase(ArchitecturePhase.Destroying);
|
||||
|
||||
// 销毁所有实现了 IAsyncDestroyable 或 IDestroyable 的组件(按注册逆序销毁)
|
||||
_logger.Info($"Destroying {_disposables.Count} disposable components");
|
||||
|
||||
for (var i = _disposables.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var component = _disposables[i];
|
||||
try
|
||||
{
|
||||
_logger.Debug($"Destroying component: {component.GetType().Name}");
|
||||
|
||||
// 优先使用异步销毁
|
||||
if (component is IAsyncDestroyable asyncDestroyable)
|
||||
{
|
||||
await asyncDestroyable.DestroyAsync();
|
||||
}
|
||||
else if (component is IDestroyable destroyable)
|
||||
{
|
||||
destroyable.Destroy();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error destroying {component.GetType().Name}", ex);
|
||||
// 继续销毁其他组件,不会因为一个组件失败而中断
|
||||
}
|
||||
}
|
||||
|
||||
_disposables.Clear();
|
||||
_disposableSet.Clear();
|
||||
|
||||
// 销毁服务模块
|
||||
await Services.ModuleManager.DestroyAllAsync();
|
||||
|
||||
Container.Clear();
|
||||
|
||||
// 进入已销毁阶段
|
||||
EnterPhase(ArchitecturePhase.Destroyed);
|
||||
_logger.Info("Architecture destruction completed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容)
|
||||
/// </summary>
|
||||
[Obsolete("建议使用 DestroyAsync() 以支持异步清理")]
|
||||
public virtual void Destroy()
|
||||
{
|
||||
DestroyAsync().AsTask().GetAwaiter().GetResult();
|
||||
return _modules.InstallModule(module);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -423,177 +173,77 @@ public abstract class Architecture(
|
||||
#region Component Registration
|
||||
|
||||
/// <summary>
|
||||
/// 验证是否允许注册组件
|
||||
/// 注册一个系统到架构中
|
||||
/// </summary>
|
||||
/// <param name="componentType">组件类型描述</param>
|
||||
/// <exception cref="InvalidOperationException">当不允许注册时抛出</exception>
|
||||
private void ValidateRegistration(string componentType)
|
||||
{
|
||||
if (CurrentPhase < ArchitecturePhase.Ready ||
|
||||
Configuration.ArchitectureProperties.AllowLateRegistration) return;
|
||||
var errorMsg = $"Cannot register {componentType} after Architecture is Ready";
|
||||
_logger.Error(errorMsg);
|
||||
throw new InvalidOperationException(errorMsg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个系统到架构中。
|
||||
/// 若当前未初始化,则暂存至待初始化列表;否则立即初始化该系统。
|
||||
/// </summary>
|
||||
/// <typeparam name="TSystem">要注册的系统类型,必须实现ISystem接口</typeparam>
|
||||
/// <typeparam name="TSystem">要注册的系统类型</typeparam>
|
||||
/// <param name="system">要注册的系统实例</param>
|
||||
/// <returns>注册成功的系统实例</returns>
|
||||
public TSystem RegisterSystem<TSystem>(TSystem system) where TSystem : ISystem
|
||||
{
|
||||
ValidateRegistration("system");
|
||||
|
||||
_logger.Debug($"Registering system: {typeof(TSystem).Name}");
|
||||
|
||||
system.SetContext(Context);
|
||||
Container.RegisterPlurality(system);
|
||||
|
||||
// 处理生命周期
|
||||
RegisterLifecycleComponent(system);
|
||||
|
||||
_logger.Info($"System registered: {typeof(TSystem).Name}");
|
||||
return system;
|
||||
return _componentRegistry.RegisterSystem(system);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册系统类型,由 DI 容器自动创建实例
|
||||
/// 注册系统类型,由 DI 容器自动创建实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T">系统类型</typeparam>
|
||||
/// <param name="onCreated">可选的实例创建后回调,用于自定义配置</param>
|
||||
/// <param name="onCreated">可选的实例创建后回调</param>
|
||||
public void RegisterSystem<T>(Action<T>? onCreated = null) where T : class, ISystem
|
||||
{
|
||||
ValidateRegistration("system");
|
||||
_logger.Debug($"Registering system type: {typeof(T).Name}");
|
||||
|
||||
Container.RegisterFactory<T>(sp =>
|
||||
{
|
||||
// 1. DI 创建实例
|
||||
var system = ActivatorUtilities.CreateInstance<T>(sp);
|
||||
|
||||
// 2. 框架默认处理
|
||||
system.SetContext(Context);
|
||||
RegisterLifecycleComponent(system);
|
||||
|
||||
// 3. 用户自定义处理(钩子)
|
||||
onCreated?.Invoke(system);
|
||||
|
||||
_logger.Debug($"System created: {typeof(T).Name}");
|
||||
return system;
|
||||
});
|
||||
|
||||
_logger.Info($"System type registered: {typeof(T).Name}");
|
||||
_componentRegistry.RegisterSystem(onCreated);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个模型到架构中。
|
||||
/// 若当前未初始化,则暂存至待初始化列表;否则立即初始化该模型。
|
||||
/// 注册一个模型到架构中
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">要注册的模型类型,必须实现IModel接口</typeparam>
|
||||
/// <typeparam name="TModel">要注册的模型类型</typeparam>
|
||||
/// <param name="model">要注册的模型实例</param>
|
||||
/// <returns>注册成功的模型实例</returns>
|
||||
public TModel RegisterModel<TModel>(TModel model) where TModel : IModel
|
||||
{
|
||||
ValidateRegistration("model");
|
||||
|
||||
_logger.Debug($"Registering model: {typeof(TModel).Name}");
|
||||
|
||||
model.SetContext(Context);
|
||||
Container.RegisterPlurality(model);
|
||||
|
||||
// 处理生命周期
|
||||
RegisterLifecycleComponent(model);
|
||||
|
||||
_logger.Info($"Model registered: {typeof(TModel).Name}");
|
||||
return model;
|
||||
return _componentRegistry.RegisterModel(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册模型类型,由 DI 容器自动创建实例
|
||||
/// 注册模型类型,由 DI 容器自动创建实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T">模型类型</typeparam>
|
||||
/// <param name="onCreated">可选的实例创建后回调,用于自定义配置</param>
|
||||
/// <param name="onCreated">可选的实例创建后回调</param>
|
||||
public void RegisterModel<T>(Action<T>? onCreated = null) where T : class, IModel
|
||||
{
|
||||
ValidateRegistration("model");
|
||||
_logger.Debug($"Registering model type: {typeof(T).Name}");
|
||||
|
||||
Container.RegisterFactory<T>(sp =>
|
||||
{
|
||||
var model = ActivatorUtilities.CreateInstance<T>(sp);
|
||||
model.SetContext(Context);
|
||||
RegisterLifecycleComponent(model);
|
||||
|
||||
// 用户自定义钩子
|
||||
onCreated?.Invoke(model);
|
||||
|
||||
_logger.Debug($"Model created: {typeof(T).Name}");
|
||||
return model;
|
||||
});
|
||||
|
||||
_logger.Info($"Model type registered: {typeof(T).Name}");
|
||||
_componentRegistry.RegisterModel(onCreated);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个工具到架构中
|
||||
/// </summary>
|
||||
/// <typeparam name="TUtility">要注册的工具类型,必须实现IUtility接口</typeparam>
|
||||
/// <typeparam name="TUtility">要注册的工具类型</typeparam>
|
||||
/// <param name="utility">要注册的工具实例</param>
|
||||
/// <returns>注册成功的工具实例</returns>
|
||||
public TUtility RegisterUtility<TUtility>(TUtility utility) where TUtility : IUtility
|
||||
{
|
||||
_logger.Debug($"Registering utility: {typeof(TUtility).Name}");
|
||||
|
||||
// 处理上下文工具类型的设置和生命周期管理
|
||||
utility.IfType<IContextUtility>(contextUtility =>
|
||||
{
|
||||
contextUtility.SetContext(Context);
|
||||
// 处理生命周期
|
||||
RegisterLifecycleComponent(contextUtility);
|
||||
});
|
||||
|
||||
Container.RegisterPlurality(utility);
|
||||
_logger.Info($"Utility registered: {typeof(TUtility).Name}");
|
||||
return utility;
|
||||
return _componentRegistry.RegisterUtility(utility);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册工具类型,由 DI 容器自动创建实例
|
||||
/// 注册工具类型,由 DI 容器自动创建实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T">工具类型</typeparam>
|
||||
/// <param name="onCreated">可选的实例创建后回调,用于自定义配置</param>
|
||||
/// <param name="onCreated">可选的实例创建后回调</param>
|
||||
public void RegisterUtility<T>(Action<T>? onCreated = null) where T : class, IUtility
|
||||
{
|
||||
_logger.Debug($"Registering utility type: {typeof(T).Name}");
|
||||
|
||||
Container.RegisterFactory<T>(sp =>
|
||||
{
|
||||
var utility = ActivatorUtilities.CreateInstance<T>(sp);
|
||||
|
||||
// 如果是 IContextUtility,设置上下文
|
||||
if (utility is IContextUtility contextUtility)
|
||||
{
|
||||
contextUtility.SetContext(Context);
|
||||
RegisterLifecycleComponent(contextUtility);
|
||||
}
|
||||
|
||||
// 用户自定义钩子
|
||||
onCreated?.Invoke(utility);
|
||||
|
||||
_logger.Debug($"Utility created: {typeof(T).Name}");
|
||||
return utility;
|
||||
});
|
||||
|
||||
_logger.Info($"Utility type registered: {typeof(T).Name}");
|
||||
_componentRegistry.RegisterUtility(onCreated);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
/// <summary>
|
||||
/// 抽象初始化方法,由子类重写以进行自定义初始化操作
|
||||
/// </summary>
|
||||
protected abstract void OnInitialize();
|
||||
|
||||
/// <summary>
|
||||
/// 同步初始化方法,阻塞当前线程直到初始化完成
|
||||
/// </summary>
|
||||
@ -606,7 +256,7 @@ public abstract class Architecture(
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error("Architecture initialization failed:", e);
|
||||
EnterPhase(ArchitecturePhase.FailedInitialization);
|
||||
_lifecycle.MarkAsFailed(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@ -614,7 +264,6 @@ public abstract class Architecture(
|
||||
/// <summary>
|
||||
/// 异步初始化方法,返回Task以便调用者可以等待初始化完成
|
||||
/// </summary>
|
||||
/// <returns>表示异步初始化操作的Task</returns>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
@ -624,32 +273,29 @@ public abstract class Architecture(
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error("Architecture initialization failed:", e);
|
||||
EnterPhase(ArchitecturePhase.FailedInitialization);
|
||||
_lifecycle.MarkAsFailed(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步初始化架构内部组件,包括上下文、模型和系统的初始化
|
||||
/// 异步初始化架构内部组件
|
||||
/// </summary>
|
||||
/// <param name="asyncMode">是否启用异步模式进行组件初始化</param>
|
||||
/// <returns>异步任务,表示初始化操作的完成</returns>
|
||||
/// <param name="asyncMode">是否启用异步模式</param>
|
||||
private async Task InitializeInternalAsync(bool asyncMode)
|
||||
{
|
||||
// === 基础上下文 & Logger ===
|
||||
LoggerFactoryResolver.Provider = Configuration.LoggerProperties.LoggerFactoryProvider;
|
||||
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
|
||||
// === 基础环境初始化 ===
|
||||
Environment.Initialize();
|
||||
|
||||
// 注册内置服务模块
|
||||
Services.ModuleManager.RegisterBuiltInModules(Container);
|
||||
Services.ModuleManager.RegisterBuiltInModules(Services.Container);
|
||||
|
||||
// 将 Environment 注册到容器(如果尚未注册)
|
||||
if (!Container.Contains<IEnvironment>())
|
||||
Container.RegisterPlurality(Environment);
|
||||
// 将 Environment 注册到容器
|
||||
if (!Services.Container.Contains<IEnvironment>())
|
||||
Services.Container.RegisterPlurality(Environment);
|
||||
|
||||
// 初始化架构上下文(如果尚未初始化)
|
||||
_context ??= new ArchitectureContext(Container);
|
||||
// 初始化架构上下文
|
||||
_context ??= new ArchitectureContext(Services.Container);
|
||||
GameContext.Bind(GetType(), _context);
|
||||
|
||||
// 为服务设置上下文
|
||||
@ -660,7 +306,7 @@ public abstract class Architecture(
|
||||
}
|
||||
|
||||
// 执行服务钩子
|
||||
Container.ExecuteServicesHook(Configurator);
|
||||
Services.Container.ExecuteServicesHook(Configurator);
|
||||
|
||||
// 初始化服务模块
|
||||
await Services.ModuleManager.InitializeAllAsync(asyncMode);
|
||||
@ -671,39 +317,44 @@ public abstract class Architecture(
|
||||
_logger.Debug("User OnInitialize() completed");
|
||||
|
||||
// === 组件初始化阶段 ===
|
||||
await InitializeAllComponentsAsync(asyncMode);
|
||||
await _lifecycle.InitializeAllComponentsAsync(asyncMode);
|
||||
|
||||
// === 初始化完成阶段 ===
|
||||
Container.Freeze();
|
||||
Services.Container.Freeze();
|
||||
_logger.Info("IOC container frozen");
|
||||
|
||||
_mInitialized = true;
|
||||
EnterPhase(ArchitecturePhase.Ready);
|
||||
// 🔥 释放 Ready await
|
||||
_readyTcs.TrySetResult();
|
||||
|
||||
_lifecycle.MarkAsReady();
|
||||
_logger.Info($"Architecture {GetType().Name} is ready - all components initialized");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待架构初始化完成(Ready 阶段)
|
||||
/// 如果架构已经处于就绪状态,则立即返回已完成的任务;
|
||||
/// 否则返回一个任务,该任务将在架构进入就绪状态时完成。
|
||||
/// </summary>
|
||||
/// <returns>表示等待操作的Task对象</returns>
|
||||
public Task WaitUntilReadyAsync()
|
||||
{
|
||||
return IsReady ? Task.CompletedTask : _readyTcs.Task;
|
||||
return _lifecycle.WaitUntilReadyAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Destruction
|
||||
|
||||
/// <summary>
|
||||
/// 异步销毁架构及所有组件
|
||||
/// </summary>
|
||||
public virtual async ValueTask DestroyAsync()
|
||||
{
|
||||
await _lifecycle.DestroyAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用于配置服务集合的委托
|
||||
/// 默认实现返回null,子类可以重写此属性以提供自定义配置逻辑
|
||||
/// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容)
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// 一个可为空的Action委托,用于配置IServiceCollection实例
|
||||
/// </value>
|
||||
public virtual Action<IServiceCollection>? Configurator => null;
|
||||
[Obsolete("建议使用 DestroyAsync() 以支持异步清理")]
|
||||
public virtual void Destroy()
|
||||
{
|
||||
_lifecycle.Destroy();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
204
GFramework.Core/Architectures/ArchitectureComponentRegistry.cs
Normal file
204
GFramework.Core/Architectures/ArchitectureComponentRegistry.cs
Normal file
@ -0,0 +1,204 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Enums;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
using GFramework.Core.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GFramework.Core.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 架构组件注册管理器
|
||||
/// 负责管理 System、Model、Utility 的注册
|
||||
/// </summary>
|
||||
internal sealed class ArchitectureComponentRegistry(
|
||||
IArchitecture architecture,
|
||||
IArchitectureConfiguration configuration,
|
||||
IArchitectureServices services,
|
||||
ArchitectureLifecycle lifecycle,
|
||||
ILogger logger)
|
||||
{
|
||||
#region Validation
|
||||
|
||||
/// <summary>
|
||||
/// 验证是否允许注册组件
|
||||
/// </summary>
|
||||
/// <param name="componentType">组件类型描述</param>
|
||||
/// <exception cref="InvalidOperationException">当不允许注册时抛出</exception>
|
||||
private void ValidateRegistration(string componentType)
|
||||
{
|
||||
if (lifecycle.CurrentPhase < ArchitecturePhase.Ready ||
|
||||
configuration.ArchitectureProperties.AllowLateRegistration) return;
|
||||
var errorMsg = $"Cannot register {componentType} after Architecture is Ready";
|
||||
logger.Error(errorMsg);
|
||||
throw new InvalidOperationException(errorMsg);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region System Registration
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个系统到架构中
|
||||
/// </summary>
|
||||
/// <typeparam name="TSystem">要注册的系统类型</typeparam>
|
||||
/// <param name="system">要注册的系统实例</param>
|
||||
/// <returns>注册成功的系统实例</returns>
|
||||
public TSystem RegisterSystem<TSystem>(TSystem system) where TSystem : ISystem
|
||||
{
|
||||
ValidateRegistration("system");
|
||||
|
||||
logger.Debug($"Registering system: {typeof(TSystem).Name}");
|
||||
|
||||
system.SetContext(architecture.Context);
|
||||
services.Container.RegisterPlurality(system);
|
||||
|
||||
// 处理生命周期
|
||||
lifecycle.RegisterLifecycleComponent(system);
|
||||
|
||||
logger.Info($"System registered: {typeof(TSystem).Name}");
|
||||
return system;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册系统类型,由 DI 容器自动创建实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T">系统类型</typeparam>
|
||||
/// <param name="onCreated">可选的实例创建后回调</param>
|
||||
public void RegisterSystem<T>(Action<T>? onCreated = null) where T : class, ISystem
|
||||
{
|
||||
ValidateRegistration("system");
|
||||
logger.Debug($"Registering system type: {typeof(T).Name}");
|
||||
|
||||
services.Container.RegisterFactory<T>(sp =>
|
||||
{
|
||||
// 1. DI 创建实例
|
||||
var system = ActivatorUtilities.CreateInstance<T>(sp);
|
||||
|
||||
// 2. 框架默认处理
|
||||
system.SetContext(architecture.Context);
|
||||
lifecycle.RegisterLifecycleComponent(system);
|
||||
|
||||
// 3. 用户自定义处理(钩子)
|
||||
onCreated?.Invoke(system);
|
||||
|
||||
logger.Debug($"System created: {typeof(T).Name}");
|
||||
return system;
|
||||
});
|
||||
|
||||
logger.Info($"System type registered: {typeof(T).Name}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Model Registration
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个模型到架构中
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">要注册的模型类型</typeparam>
|
||||
/// <param name="model">要注册的模型实例</param>
|
||||
/// <returns>注册成功的模型实例</returns>
|
||||
public TModel RegisterModel<TModel>(TModel model) where TModel : IModel
|
||||
{
|
||||
ValidateRegistration("model");
|
||||
|
||||
logger.Debug($"Registering model: {typeof(TModel).Name}");
|
||||
|
||||
model.SetContext(architecture.Context);
|
||||
services.Container.RegisterPlurality(model);
|
||||
|
||||
// 处理生命周期
|
||||
lifecycle.RegisterLifecycleComponent(model);
|
||||
|
||||
logger.Info($"Model registered: {typeof(TModel).Name}");
|
||||
return model;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册模型类型,由 DI 容器自动创建实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T">模型类型</typeparam>
|
||||
/// <param name="onCreated">可选的实例创建后回调</param>
|
||||
public void RegisterModel<T>(Action<T>? onCreated = null) where T : class, IModel
|
||||
{
|
||||
ValidateRegistration("model");
|
||||
logger.Debug($"Registering model type: {typeof(T).Name}");
|
||||
|
||||
services.Container.RegisterFactory<T>(sp =>
|
||||
{
|
||||
var model = ActivatorUtilities.CreateInstance<T>(sp);
|
||||
model.SetContext(architecture.Context);
|
||||
lifecycle.RegisterLifecycleComponent(model);
|
||||
|
||||
// 用户自定义钩子
|
||||
onCreated?.Invoke(model);
|
||||
|
||||
logger.Debug($"Model created: {typeof(T).Name}");
|
||||
return model;
|
||||
});
|
||||
|
||||
logger.Info($"Model type registered: {typeof(T).Name}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility Registration
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个工具到架构中
|
||||
/// </summary>
|
||||
/// <typeparam name="TUtility">要注册的工具类型</typeparam>
|
||||
/// <param name="utility">要注册的工具实例</param>
|
||||
/// <returns>注册成功的工具实例</returns>
|
||||
public TUtility RegisterUtility<TUtility>(TUtility utility) where TUtility : IUtility
|
||||
{
|
||||
logger.Debug($"Registering utility: {typeof(TUtility).Name}");
|
||||
|
||||
// 处理上下文工具类型的设置和生命周期管理
|
||||
utility.IfType<IContextUtility>(contextUtility =>
|
||||
{
|
||||
contextUtility.SetContext(architecture.Context);
|
||||
// 处理生命周期
|
||||
lifecycle.RegisterLifecycleComponent(contextUtility);
|
||||
});
|
||||
|
||||
services.Container.RegisterPlurality(utility);
|
||||
logger.Info($"Utility registered: {typeof(TUtility).Name}");
|
||||
return utility;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册工具类型,由 DI 容器自动创建实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T">工具类型</typeparam>
|
||||
/// <param name="onCreated">可选的实例创建后回调</param>
|
||||
public void RegisterUtility<T>(Action<T>? onCreated = null) where T : class, IUtility
|
||||
{
|
||||
logger.Debug($"Registering utility type: {typeof(T).Name}");
|
||||
|
||||
services.Container.RegisterFactory<T>(sp =>
|
||||
{
|
||||
var utility = ActivatorUtilities.CreateInstance<T>(sp);
|
||||
|
||||
// 如果是 IContextUtility,设置上下文
|
||||
if (utility is IContextUtility contextUtility)
|
||||
{
|
||||
contextUtility.SetContext(architecture.Context);
|
||||
lifecycle.RegisterLifecycleComponent(contextUtility);
|
||||
}
|
||||
|
||||
// 用户自定义钩子
|
||||
onCreated?.Invoke(utility);
|
||||
|
||||
logger.Debug($"Utility created: {typeof(T).Name}");
|
||||
return utility;
|
||||
});
|
||||
|
||||
logger.Info($"Utility type registered: {typeof(T).Name}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
420
GFramework.Core/Architectures/ArchitectureLifecycle.cs
Normal file
420
GFramework.Core/Architectures/ArchitectureLifecycle.cs
Normal file
@ -0,0 +1,420 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Enums;
|
||||
using GFramework.Core.Abstractions.Lifecycle;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
|
||||
namespace GFramework.Core.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 架构生命周期管理器
|
||||
/// 负责管理架构的阶段转换、组件初始化和销毁
|
||||
/// </summary>
|
||||
internal sealed class ArchitectureLifecycle(
|
||||
IArchitecture architecture,
|
||||
IArchitectureConfiguration configuration,
|
||||
IArchitectureServices services,
|
||||
ILogger logger)
|
||||
{
|
||||
#region Lifecycle Hook Management
|
||||
|
||||
/// <summary>
|
||||
/// 注册生命周期钩子
|
||||
/// </summary>
|
||||
/// <param name="hook">生命周期钩子实例</param>
|
||||
/// <returns>注册的钩子实例</returns>
|
||||
public IArchitectureLifecycleHook RegisterLifecycleHook(IArchitectureLifecycleHook hook)
|
||||
{
|
||||
if (CurrentPhase >= ArchitecturePhase.Ready && !configuration.ArchitectureProperties.AllowLateRegistration)
|
||||
throw new InvalidOperationException(
|
||||
"Cannot register lifecycle hook after architecture is Ready");
|
||||
_lifecycleHooks.Add(hook);
|
||||
return hook;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Component Lifecycle Management
|
||||
|
||||
/// <summary>
|
||||
/// 统一的组件生命周期注册逻辑
|
||||
/// </summary>
|
||||
/// <param name="component">要注册的组件</param>
|
||||
public void RegisterLifecycleComponent(object component)
|
||||
{
|
||||
// 处理初始化
|
||||
if (component is IInitializable initializable)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
// 原子去重:HashSet.Add 返回 true 表示添加成功(之前不存在)
|
||||
if (_pendingInitializableSet.Add(initializable))
|
||||
{
|
||||
_pendingInitializableList.Add(initializable);
|
||||
logger.Trace($"Added {component.GetType().Name} to pending initialization queue");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cannot initialize component after Architecture is Ready");
|
||||
}
|
||||
}
|
||||
|
||||
// 处理销毁(支持 IDestroyable 或 IAsyncDestroyable)
|
||||
if (component is not (IDestroyable or IAsyncDestroyable)) return;
|
||||
// 原子去重:HashSet.Add 返回 true 表示添加成功(之前不存在)
|
||||
if (!_disposableSet.Add(component)) return;
|
||||
_disposables.Add(component);
|
||||
logger.Trace($"Registered {component.GetType().Name} for destruction");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
private readonly TaskCompletionSource _readyTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
/// <summary>
|
||||
/// 待初始化组件的去重集合
|
||||
/// </summary>
|
||||
private readonly HashSet<IInitializable> _pendingInitializableSet = [];
|
||||
|
||||
/// <summary>
|
||||
/// 存储所有待初始化的组件(统一管理,保持注册顺序)
|
||||
/// </summary>
|
||||
private readonly List<IInitializable> _pendingInitializableList = [];
|
||||
|
||||
/// <summary>
|
||||
/// 可销毁组件的去重集合(支持 IDestroyable 和 IAsyncDestroyable)
|
||||
/// </summary>
|
||||
private readonly HashSet<object> _disposableSet = [];
|
||||
|
||||
/// <summary>
|
||||
/// 存储所有需要销毁的组件(统一管理,保持注册逆序销毁)
|
||||
/// </summary>
|
||||
private readonly List<object> _disposables = [];
|
||||
|
||||
/// <summary>
|
||||
/// 生命周期感知对象列表
|
||||
/// </summary>
|
||||
private readonly List<IArchitectureLifecycleHook> _lifecycleHooks = [];
|
||||
|
||||
/// <summary>
|
||||
/// 标记架构是否已初始化完成
|
||||
/// </summary>
|
||||
private bool _initialized;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// 当前架构的阶段
|
||||
/// </summary>
|
||||
public ArchitecturePhase CurrentPhase { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个布尔值,指示当前架构是否处于就绪状态
|
||||
/// </summary>
|
||||
public bool IsReady => CurrentPhase == ArchitecturePhase.Ready;
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个布尔值,指示架构是否已初始化
|
||||
/// </summary>
|
||||
public bool IsInitialized => _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// 阶段变更事件(用于测试和扩展)
|
||||
/// </summary>
|
||||
public event Action<ArchitecturePhase>? PhaseChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Phase Management
|
||||
|
||||
/// <summary>
|
||||
/// 进入指定的架构阶段,并执行相应的生命周期管理操作
|
||||
/// </summary>
|
||||
/// <param name="next">要进入的下一个架构阶段</param>
|
||||
/// <exception cref="InvalidOperationException">当阶段转换不被允许时抛出异常</exception>
|
||||
public void EnterPhase(ArchitecturePhase next)
|
||||
{
|
||||
// 验证阶段转换
|
||||
ValidatePhaseTransition(next);
|
||||
|
||||
// 执行阶段转换
|
||||
var previousPhase = CurrentPhase;
|
||||
CurrentPhase = next;
|
||||
|
||||
if (previousPhase != next)
|
||||
logger.Info($"Architecture phase changed: {previousPhase} -> {next}");
|
||||
|
||||
// 通知阶段变更
|
||||
NotifyPhase(next);
|
||||
NotifyPhaseAwareObjects(next);
|
||||
|
||||
// 触发阶段变更事件(用于测试和扩展)
|
||||
PhaseChanged?.Invoke(next);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证阶段转换是否合法
|
||||
/// </summary>
|
||||
/// <param name="next">目标阶段</param>
|
||||
/// <exception cref="InvalidOperationException">当阶段转换不合法时抛出</exception>
|
||||
private void ValidatePhaseTransition(ArchitecturePhase next)
|
||||
{
|
||||
// 不需要严格验证,直接返回
|
||||
if (!configuration.ArchitectureProperties.StrictPhaseValidation)
|
||||
return;
|
||||
|
||||
// FailedInitialization 可以从任何阶段转换,直接返回
|
||||
if (next == ArchitecturePhase.FailedInitialization)
|
||||
return;
|
||||
|
||||
// 检查转换是否在允许列表中
|
||||
if (ArchitectureConstants.PhaseTransitions.TryGetValue(CurrentPhase, out var allowed) &&
|
||||
allowed.Contains(next))
|
||||
return;
|
||||
|
||||
// 转换不合法,抛出异常
|
||||
var errorMsg = $"Invalid phase transition: {CurrentPhase} -> {next}";
|
||||
logger.Fatal(errorMsg);
|
||||
throw new InvalidOperationException(errorMsg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知所有架构阶段感知对象阶段变更
|
||||
/// </summary>
|
||||
/// <param name="phase">新阶段</param>
|
||||
private void NotifyPhaseAwareObjects(ArchitecturePhase phase)
|
||||
{
|
||||
foreach (var obj in services.Container.GetAll<IArchitecturePhaseListener>())
|
||||
{
|
||||
logger.Trace($"Notifying phase-aware object {obj.GetType().Name} of phase change to {phase}");
|
||||
obj.OnArchitecturePhase(phase);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知所有生命周期钩子当前阶段变更
|
||||
/// </summary>
|
||||
/// <param name="phase">当前架构阶段</param>
|
||||
private void NotifyPhase(ArchitecturePhase phase)
|
||||
{
|
||||
foreach (var hook in _lifecycleHooks)
|
||||
{
|
||||
hook.OnPhase(phase, architecture);
|
||||
logger.Trace($"Notifying lifecycle hook {hook.GetType().Name} of phase {phase}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
/// <summary>
|
||||
/// 初始化所有待初始化的组件
|
||||
/// </summary>
|
||||
/// <param name="asyncMode">是否使用异步模式</param>
|
||||
public async Task InitializeAllComponentsAsync(bool asyncMode)
|
||||
{
|
||||
logger.Info($"Initializing {_pendingInitializableList.Count} components");
|
||||
|
||||
// 按类型分组初始化(保持原有的阶段划分)
|
||||
var utilities = _pendingInitializableList.OfType<IContextUtility>().ToList();
|
||||
var models = _pendingInitializableList.OfType<IModel>().ToList();
|
||||
var systems = _pendingInitializableList.OfType<ISystem>().ToList();
|
||||
|
||||
// 1. 工具初始化阶段
|
||||
EnterPhase(ArchitecturePhase.BeforeUtilityInit);
|
||||
|
||||
if (utilities.Count != 0)
|
||||
{
|
||||
logger.Info($"Initializing {utilities.Count} context utilities");
|
||||
|
||||
foreach (var utility in utilities)
|
||||
{
|
||||
logger.Debug($"Initializing utility: {utility.GetType().Name}");
|
||||
await InitializeComponentAsync(utility, asyncMode);
|
||||
}
|
||||
|
||||
logger.Info("All context utilities initialized");
|
||||
}
|
||||
|
||||
EnterPhase(ArchitecturePhase.AfterUtilityInit);
|
||||
|
||||
// 2. 模型初始化阶段
|
||||
EnterPhase(ArchitecturePhase.BeforeModelInit);
|
||||
|
||||
if (models.Count != 0)
|
||||
{
|
||||
logger.Info($"Initializing {models.Count} models");
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
logger.Debug($"Initializing model: {model.GetType().Name}");
|
||||
await InitializeComponentAsync(model, asyncMode);
|
||||
}
|
||||
|
||||
logger.Info("All models initialized");
|
||||
}
|
||||
|
||||
EnterPhase(ArchitecturePhase.AfterModelInit);
|
||||
|
||||
// 3. 系统初始化阶段
|
||||
EnterPhase(ArchitecturePhase.BeforeSystemInit);
|
||||
|
||||
if (systems.Count != 0)
|
||||
{
|
||||
logger.Info($"Initializing {systems.Count} systems");
|
||||
|
||||
foreach (var system in systems)
|
||||
{
|
||||
logger.Debug($"Initializing system: {system.GetType().Name}");
|
||||
await InitializeComponentAsync(system, asyncMode);
|
||||
}
|
||||
|
||||
logger.Info("All systems initialized");
|
||||
}
|
||||
|
||||
EnterPhase(ArchitecturePhase.AfterSystemInit);
|
||||
|
||||
_pendingInitializableList.Clear();
|
||||
_pendingInitializableSet.Clear();
|
||||
_initialized = true;
|
||||
logger.Info("All components initialized");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步初始化单个组件
|
||||
/// </summary>
|
||||
/// <param name="component">要初始化的组件</param>
|
||||
/// <param name="asyncMode">是否使用异步模式</param>
|
||||
private static async Task InitializeComponentAsync(IInitializable component, bool asyncMode)
|
||||
{
|
||||
if (asyncMode && component is IAsyncInitializable asyncInit)
|
||||
await asyncInit.InitializeAsync();
|
||||
else
|
||||
component.Initialize();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Destruction
|
||||
|
||||
/// <summary>
|
||||
/// 异步销毁架构及所有组件
|
||||
/// </summary>
|
||||
public async ValueTask DestroyAsync()
|
||||
{
|
||||
// 检查当前阶段,如果已经处于销毁或已销毁状态则直接返回
|
||||
if (CurrentPhase >= ArchitecturePhase.Destroying)
|
||||
{
|
||||
logger.Warn("Architecture destroy called but already in destroying/destroyed state");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果从未初始化(None 阶段),只清理已注册的组件,不进行阶段转换
|
||||
if (CurrentPhase == ArchitecturePhase.None)
|
||||
{
|
||||
logger.Debug("Architecture destroy called but never initialized, cleaning up registered components");
|
||||
await CleanupComponentsAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// 进入销毁阶段
|
||||
logger.Info("Starting architecture destruction");
|
||||
EnterPhase(ArchitecturePhase.Destroying);
|
||||
|
||||
// 清理所有组件
|
||||
await CleanupComponentsAsync();
|
||||
|
||||
// 销毁服务模块
|
||||
await services.ModuleManager.DestroyAllAsync();
|
||||
|
||||
services.Container.Clear();
|
||||
|
||||
// 进入已销毁阶段
|
||||
EnterPhase(ArchitecturePhase.Destroyed);
|
||||
logger.Info("Architecture destruction completed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理所有已注册的可销毁组件
|
||||
/// </summary>
|
||||
private async ValueTask CleanupComponentsAsync()
|
||||
{
|
||||
// 销毁所有实现了 IAsyncDestroyable 或 IDestroyable 的组件(按注册逆序销毁)
|
||||
logger.Info($"Destroying {_disposables.Count} disposable components");
|
||||
|
||||
for (var i = _disposables.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var component = _disposables[i];
|
||||
try
|
||||
{
|
||||
logger.Debug($"Destroying component: {component.GetType().Name}");
|
||||
|
||||
// 优先使用异步销毁
|
||||
if (component is IAsyncDestroyable asyncDestroyable)
|
||||
{
|
||||
await asyncDestroyable.DestroyAsync();
|
||||
}
|
||||
else if (component is IDestroyable destroyable)
|
||||
{
|
||||
destroyable.Destroy();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Error destroying {component.GetType().Name}", ex);
|
||||
// 继续销毁其他组件,不会因为一个组件失败而中断
|
||||
}
|
||||
}
|
||||
|
||||
_disposables.Clear();
|
||||
_disposableSet.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容)
|
||||
/// </summary>
|
||||
[Obsolete("建议使用 DestroyAsync() 以支持异步清理")]
|
||||
public void Destroy()
|
||||
{
|
||||
DestroyAsync().AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Ready State
|
||||
|
||||
/// <summary>
|
||||
/// 标记架构为就绪状态
|
||||
/// </summary>
|
||||
public void MarkAsReady()
|
||||
{
|
||||
EnterPhase(ArchitecturePhase.Ready);
|
||||
_readyTcs.TrySetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记架构初始化失败
|
||||
/// </summary>
|
||||
/// <param name="exception">失败异常</param>
|
||||
public void MarkAsFailed(Exception exception)
|
||||
{
|
||||
EnterPhase(ArchitecturePhase.FailedInitialization);
|
||||
_readyTcs.TrySetException(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 等待架构就绪
|
||||
/// </summary>
|
||||
public Task WaitUntilReadyAsync() => _readyTcs.Task;
|
||||
|
||||
#endregion
|
||||
}
|
||||
39
GFramework.Core/Architectures/ArchitectureModules.cs
Normal file
39
GFramework.Core/Architectures/ArchitectureModules.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using GFramework.Core.Abstractions.Architectures;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
namespace GFramework.Core.Architectures;
|
||||
|
||||
/// <summary>
|
||||
/// 架构模块管理器
|
||||
/// 负责管理架构模块的安装和中介行为注册
|
||||
/// </summary>
|
||||
internal sealed class ArchitectureModules(
|
||||
IArchitecture architecture,
|
||||
IArchitectureServices services,
|
||||
ILogger logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册中介行为管道
|
||||
/// 用于配置Mediator框架的行为拦截和处理逻辑
|
||||
/// </summary>
|
||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
||||
{
|
||||
logger.Debug($"Registering mediator behavior: {typeof(TBehavior).Name}");
|
||||
services.Container.RegisterMediatorBehavior<TBehavior>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安装架构模块
|
||||
/// </summary>
|
||||
/// <param name="module">要安装的模块</param>
|
||||
/// <returns>安装的模块实例</returns>
|
||||
public IArchitectureModule InstallModule(IArchitectureModule module)
|
||||
{
|
||||
var name = module.GetType().Name;
|
||||
logger.Debug($"Installing module: {name}");
|
||||
module.Install(architecture);
|
||||
logger.Info($"Module installed: {name}");
|
||||
return module;
|
||||
}
|
||||
}
|
||||
@ -17,11 +17,6 @@ internal sealed class CoroutineSlot
|
||||
/// </summary>
|
||||
public CoroutineHandle Handle;
|
||||
|
||||
/// <summary>
|
||||
/// 协程是否已经开始执行
|
||||
/// </summary>
|
||||
public bool HasStarted;
|
||||
|
||||
/// <summary>
|
||||
/// 协程的优先级
|
||||
/// </summary>
|
||||
|
||||
@ -18,6 +18,11 @@ public class PriorityEvent<T> : IEvent
|
||||
/// </summary>
|
||||
private readonly List<EventHandler> _handlers = new();
|
||||
|
||||
/// <summary>
|
||||
/// 保护处理器集合的并发访问
|
||||
/// </summary>
|
||||
private readonly object _syncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// 标记事件是否已被处理(用于 UntilHandled 传播模式)
|
||||
/// </summary>
|
||||
@ -52,10 +57,13 @@ public class PriorityEvent<T> : IEvent
|
||||
public IUnRegister Register(Action<T> onEvent, int priority)
|
||||
{
|
||||
var handler = new EventHandler(onEvent, priority);
|
||||
_handlers.Add(handler);
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_handlers.Add(handler);
|
||||
|
||||
// 按优先级降序排序(高优先级在前)
|
||||
_handlers.Sort((a, b) => b.Priority.CompareTo(a.Priority));
|
||||
// 按优先级降序排序(高优先级在前)
|
||||
_handlers.Sort((a, b) => b.Priority.CompareTo(a.Priority));
|
||||
}
|
||||
|
||||
return new DefaultUnRegister(() => UnRegister(onEvent));
|
||||
}
|
||||
@ -66,7 +74,10 @@ public class PriorityEvent<T> : IEvent
|
||||
/// <param name="onEvent">需要被注销的事件处理方法</param>
|
||||
public void UnRegister(Action<T> onEvent)
|
||||
{
|
||||
_handlers.RemoveAll(h => h.Handler == onEvent);
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_handlers.RemoveAll(h => h.Handler == onEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -78,10 +89,13 @@ public class PriorityEvent<T> : IEvent
|
||||
public IUnRegister RegisterWithContext(Action<EventContext<T>> onEvent, int priority = 0)
|
||||
{
|
||||
var handler = new ContextEventHandler(onEvent, priority);
|
||||
_contextHandlers.Add(handler);
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_contextHandlers.Add(handler);
|
||||
|
||||
// 按优先级降序排序(高优先级在前)
|
||||
_contextHandlers.Sort((a, b) => b.Priority.CompareTo(a.Priority));
|
||||
// 按优先级降序排序(高优先级在前)
|
||||
_contextHandlers.Sort((a, b) => b.Priority.CompareTo(a.Priority));
|
||||
}
|
||||
|
||||
return new DefaultUnRegister(() => UnRegisterContext(onEvent));
|
||||
}
|
||||
@ -92,7 +106,10 @@ public class PriorityEvent<T> : IEvent
|
||||
/// <param name="onEvent">需要被注销的事件处理方法</param>
|
||||
public void UnRegisterContext(Action<EventContext<T>> onEvent)
|
||||
{
|
||||
_contextHandlers.RemoveAll(h => h.Handler == onEvent);
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_contextHandlers.RemoveAll(h => h.Handler == onEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -172,8 +189,7 @@ public class PriorityEvent<T> : IEvent
|
||||
/// <param name="t">事件参数</param>
|
||||
private void TriggerHighest(T t)
|
||||
{
|
||||
var normalSnapshot = _handlers.ToArray();
|
||||
var contextSnapshot = _contextHandlers.ToArray();
|
||||
var (normalSnapshot, contextSnapshot) = CreateSnapshots();
|
||||
var highestPriority = GetHighestPriority(normalSnapshot, contextSnapshot);
|
||||
|
||||
if (highestPriority != int.MinValue)
|
||||
@ -191,15 +207,11 @@ public class PriorityEvent<T> : IEvent
|
||||
private List<(int Priority, Action? Handler, Action<EventContext<T>>? ContextHandler, bool IsContext)>
|
||||
MergeAndSortHandlers(T t)
|
||||
{
|
||||
var normalSnapshot = _handlers.ToArray();
|
||||
var contextSnapshot = _contextHandlers.ToArray();
|
||||
// 使用快照避免迭代期间修改
|
||||
var (normalSnapshot, contextSnapshot) = CreateSnapshots();
|
||||
// 使用统一的投影方法显式固定元组的可空标注,避免 LINQ 在 Concat 时推断出不兼容的签名。
|
||||
return normalSnapshot
|
||||
.Select(h => (h.Priority, Handler: (Action?)(() => h.Handler.Invoke(t)),
|
||||
ContextHandler: (Action<EventContext<T>>?)null, IsContext: false))
|
||||
.Concat(contextSnapshot
|
||||
.Select(h => (h.Priority, Handler: (Action?)null,
|
||||
ContextHandler: (Action<EventContext<T>>?)h.Handler, IsContext: true)))
|
||||
.Select(h => CreateNormalHandlerInvocation(h, t))
|
||||
.Concat(contextSnapshot.Select(CreateContextHandlerInvocation))
|
||||
.OrderByDescending(h => h.Priority)
|
||||
.ToList();
|
||||
}
|
||||
@ -260,7 +272,41 @@ public class PriorityEvent<T> : IEvent
|
||||
/// <returns>监听器总数量</returns>
|
||||
public int GetListenerCount()
|
||||
{
|
||||
return _handlers.Count + _contextHandlers.Count;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _handlers.Count + _contextHandlers.Count;
|
||||
}
|
||||
}
|
||||
|
||||
private (EventHandler[] NormalHandlers, ContextEventHandler[] ContextHandlers) CreateSnapshots()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return (_handlers.ToArray(), _contextHandlers.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将普通事件处理器转换为统一的调用描述。
|
||||
/// </summary>
|
||||
/// <param name="handler">要包装的普通处理器。</param>
|
||||
/// <param name="t">当前触发的事件数据。</param>
|
||||
/// <returns>可与上下文处理器合并排序的统一调用描述。</returns>
|
||||
private static (int Priority, Action? Handler, Action<EventContext<T>>? ContextHandler, bool IsContext)
|
||||
CreateNormalHandlerInvocation(EventHandler handler, T t)
|
||||
{
|
||||
return (handler.Priority, () => handler.Handler.Invoke(t), null, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将上下文事件处理器转换为统一的调用描述。
|
||||
/// </summary>
|
||||
/// <param name="handler">要包装的上下文处理器。</param>
|
||||
/// <returns>可与普通处理器合并排序的统一调用描述。</returns>
|
||||
private static (int Priority, Action? Handler, Action<EventContext<T>>? ContextHandler, bool IsContext)
|
||||
CreateContextHandlerInvocation(ContextEventHandler handler)
|
||||
{
|
||||
return (handler.Priority, null, handler.Handler, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
30
GFramework.Core/Extensions/NumericDisplayExtensions.cs
Normal file
30
GFramework.Core/Extensions/NumericDisplayExtensions.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.Numerics;
|
||||
using GFramework.Core.Abstractions.Utility.Numeric;
|
||||
using GFramework.Core.Utility.Numeric;
|
||||
|
||||
namespace GFramework.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 数值显示扩展方法。
|
||||
/// </summary>
|
||||
public static class NumericDisplayExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 按指定选项将数值格式化为展示字符串。
|
||||
/// </summary>
|
||||
public static string ToDisplayString<T>(this T value, NumericFormatOptions? options = null) where T : INumber<T>
|
||||
{
|
||||
return NumericDisplay.Format(value, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用默认紧凑风格将数值格式化为展示字符串。
|
||||
/// </summary>
|
||||
public static string ToCompactString<T>(
|
||||
this T value,
|
||||
int maxDecimalPlaces = 1,
|
||||
IFormatProvider? formatProvider = null) where T : INumber<T>
|
||||
{
|
||||
return NumericDisplay.FormatCompact(value, maxDecimalPlaces, formatProvider);
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@
|
||||
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4"/>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5"/>
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -15,4 +15,5 @@ global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
global using System.Threading.Tasks;
|
||||
global using System.Threading.Channels;
|
||||
@ -0,0 +1,167 @@
|
||||
using GFramework.Core.Abstractions.Localization;
|
||||
using GFramework.Core.Abstractions.Utility.Numeric;
|
||||
using GFramework.Core.Utility.Numeric;
|
||||
|
||||
namespace GFramework.Core.Localization.Formatters;
|
||||
|
||||
/// <summary>
|
||||
/// 紧凑数值格式化器。
|
||||
/// 格式: {value:compact} 或 {value:compact:maxDecimals=2,trimZeros=false}
|
||||
/// </summary>
|
||||
public sealed class CompactNumberLocalizationFormatter : ILocalizationFormatter
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取格式化器的名称
|
||||
/// </summary>
|
||||
public string Name => "compact";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将指定值按照紧凑数值格式进行格式化
|
||||
/// </summary>
|
||||
/// <param name="format">格式字符串,可包含以下选项:
|
||||
/// maxDecimals: 最大小数位数
|
||||
/// minDecimals: 最小小数位数
|
||||
/// trimZeros: 是否去除尾随零
|
||||
/// grouping: 是否在阈值以下使用分组</param>
|
||||
/// <param name="value">要格式化的数值对象</param>
|
||||
/// <param name="provider">格式提供程序,用于区域性特定的格式设置</param>
|
||||
/// <param name="result">格式化后的字符串结果</param>
|
||||
/// <returns>如果格式化成功则返回true;如果格式字符串无效或格式化失败则返回false</returns>
|
||||
public bool TryFormat(string format, object value, IFormatProvider? provider, out string result)
|
||||
{
|
||||
result = string.Empty;
|
||||
|
||||
if (!TryParseOptions(format, provider, out var options))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result = NumericDisplay.Format(value, options);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentNullException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试解析格式字符串中的选项参数
|
||||
/// </summary>
|
||||
/// <param name="format">格式字符串,包含以逗号分隔的键值对,如"maxDecimals=2,trimZeros=false"</param>
|
||||
/// <param name="provider">格式提供程序</param>
|
||||
/// <param name="options">解析成功的选项输出</param>
|
||||
/// <returns>如果所有选项都正确解析则返回true;如果有任何语法错误或无效值则返回false</returns>
|
||||
/// <remarks>
|
||||
/// 支持的选项包括:
|
||||
/// - maxDecimals: 最大小数位数,必须是有效整数
|
||||
/// - minDecimals: 最小小数位数,必须是有效整数
|
||||
/// - trimZeros: 是否去除尾随零,必须是有效布尔值
|
||||
/// - grouping: 是否在阈值以下使用分组,必须是有效布尔值
|
||||
/// 选项之间用逗号或分号分隔,格式为key=value
|
||||
/// </remarks>
|
||||
private static bool TryParseOptions(string format, IFormatProvider? provider, out NumericFormatOptions options)
|
||||
{
|
||||
options = new NumericFormatOptions
|
||||
{
|
||||
FormatProvider = provider
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var maxDecimalPlaces = options.MaxDecimalPlaces;
|
||||
var minDecimalPlaces = options.MinDecimalPlaces;
|
||||
var trimTrailingZeros = options.TrimTrailingZeros;
|
||||
var useGroupingBelowThreshold = options.UseGroupingBelowThreshold;
|
||||
|
||||
foreach (var segment in format.Split([',', ';'],
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
if (!TryParseSegment(segment, out var key, out var value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryApplyOption(
|
||||
key,
|
||||
value,
|
||||
ref maxDecimalPlaces,
|
||||
ref minDecimalPlaces,
|
||||
ref trimTrailingZeros,
|
||||
ref useGroupingBelowThreshold))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
options = options with
|
||||
{
|
||||
MaxDecimalPlaces = maxDecimalPlaces,
|
||||
MinDecimalPlaces = minDecimalPlaces,
|
||||
TrimTrailingZeros = trimTrailingZeros,
|
||||
UseGroupingBelowThreshold = useGroupingBelowThreshold
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试解析格式字符串中的单个键值对片段。
|
||||
/// </summary>
|
||||
/// <param name="segment">包含键值对的字符串片段,格式应为"key=value"</param>
|
||||
/// <param name="key">解析得到的键名</param>
|
||||
/// <param name="value">解析得到的值</param>
|
||||
/// <returns>如果片段格式有效且成功解析则返回true;如果格式无效(如缺少分隔符、空键等)则返回false</returns>
|
||||
private static bool TryParseSegment(string segment, out string key, out string value)
|
||||
{
|
||||
var separatorIndex = segment.IndexOf('=');
|
||||
if (separatorIndex <= 0 || separatorIndex == segment.Length - 1)
|
||||
{
|
||||
key = string.Empty;
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
key = segment[..separatorIndex].Trim();
|
||||
value = segment[(separatorIndex + 1)..].Trim();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将解析得到的键值对应用到相应的选项变量中。
|
||||
/// </summary>
|
||||
/// <param name="key">选项名称</param>
|
||||
/// <param name="value">选项值的字符串表示</param>
|
||||
/// <param name="maxDecimalPlaces">最大小数位数的引用参数</param>
|
||||
/// <param name="minDecimalPlaces">最小小数位数的引用参数</param>
|
||||
/// <param name="trimTrailingZeros">是否去除尾随零的引用参数</param>
|
||||
/// <param name="useGroupingBelowThreshold">是否在阈值以下使用分组的引用参数</param>
|
||||
/// <returns>如果值成功解析或键名未知则返回true;如果键名已知但值解析失败则返回false</returns>
|
||||
private static bool TryApplyOption(
|
||||
string key,
|
||||
string value,
|
||||
ref int maxDecimalPlaces,
|
||||
ref int minDecimalPlaces,
|
||||
ref bool trimTrailingZeros,
|
||||
ref bool useGroupingBelowThreshold)
|
||||
{
|
||||
return key switch
|
||||
{
|
||||
"maxDecimals" => int.TryParse(value, out maxDecimalPlaces),
|
||||
"minDecimals" => int.TryParse(value, out minDecimalPlaces),
|
||||
"trimZeros" => bool.TryParse(value, out trimTrailingZeros),
|
||||
"grouping" => bool.TryParse(value, out useGroupingBelowThreshold),
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
using GFramework.Core.Abstractions.Localization;
|
||||
|
||||
namespace GFramework.Core.Localization.Formatters;
|
||||
|
||||
/// <summary>
|
||||
/// 条件格式化器
|
||||
/// 格式: {condition:if:trueText|falseText}
|
||||
/// 示例: {upgraded:if:Upgraded|Normal}
|
||||
/// </summary>
|
||||
public class ConditionalFormatter : ILocalizationFormatter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "if";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryFormat(string format, object value, IFormatProvider? provider, out string result)
|
||||
{
|
||||
result = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var parts = format.Split('|');
|
||||
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var condition = value is bool b ? b : Convert.ToBoolean(value);
|
||||
result = condition ? parts[0] : parts[1];
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
GFramework.Core/Localization/Formatters/PluralFormatter.cs
Normal file
43
GFramework.Core/Localization/Formatters/PluralFormatter.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using GFramework.Core.Abstractions.Localization;
|
||||
|
||||
namespace GFramework.Core.Localization.Formatters;
|
||||
|
||||
/// <summary>
|
||||
/// 复数格式化器
|
||||
/// 格式: {count:plural:singular|plural}
|
||||
/// 示例: {count:plural:item|items}
|
||||
/// </summary>
|
||||
public class PluralFormatter : ILocalizationFormatter
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Name => "plural";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryFormat(string format, object value, IFormatProvider? provider, out string result)
|
||||
{
|
||||
result = string.Empty;
|
||||
|
||||
if (value is not IConvertible convertible)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var number = convertible.ToDecimal(provider);
|
||||
var parts = format.Split('|');
|
||||
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = Math.Abs(number) == 1 ? parts[0] : parts[1];
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
317
GFramework.Core/Localization/LocalizationManager.cs
Normal file
317
GFramework.Core/Localization/LocalizationManager.cs
Normal file
@ -0,0 +1,317 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using GFramework.Core.Abstractions.Localization;
|
||||
using GFramework.Core.Localization.Formatters;
|
||||
using GFramework.Core.Systems;
|
||||
|
||||
namespace GFramework.Core.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化管理器实现
|
||||
/// </summary>
|
||||
public class LocalizationManager : AbstractSystem, ILocalizationManager
|
||||
{
|
||||
private readonly List<string> _availableLanguages;
|
||||
private readonly LocalizationConfig _config;
|
||||
private readonly Dictionary<string, ILocalizationFormatter> _formatters;
|
||||
private readonly List<Action<string>> _languageChangeCallbacks;
|
||||
private readonly Dictionary<string, Dictionary<string, ILocalizationTable>> _tables;
|
||||
private CultureInfo _currentCulture;
|
||||
private string _currentLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化本地化管理器
|
||||
/// </summary>
|
||||
/// <param name="config">配置</param>
|
||||
public LocalizationManager(LocalizationConfig? config = null)
|
||||
{
|
||||
_config = config ?? new LocalizationConfig();
|
||||
_tables = new Dictionary<string, Dictionary<string, ILocalizationTable>>();
|
||||
_formatters = new Dictionary<string, ILocalizationFormatter>();
|
||||
_languageChangeCallbacks = new List<Action<string>>();
|
||||
_currentLanguage = _config.DefaultLanguage;
|
||||
_currentCulture = GetCultureInfo(_currentLanguage);
|
||||
_availableLanguages = new List<string>();
|
||||
RegisterBuiltInFormatters();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string CurrentLanguage => _currentLanguage;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CultureInfo CurrentCulture => _currentCulture;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyList<string> AvailableLanguages => _availableLanguages;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetLanguage(string languageCode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(languageCode))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(languageCode));
|
||||
}
|
||||
|
||||
if (_currentLanguage == languageCode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LoadLanguage(languageCode);
|
||||
_currentLanguage = languageCode;
|
||||
_currentCulture = GetCultureInfo(languageCode);
|
||||
|
||||
// 触发语言变化回调
|
||||
TriggerLanguageChange();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILocalizationTable GetTable(string tableName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tableName))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tableName));
|
||||
}
|
||||
|
||||
if (!_tables.TryGetValue(_currentLanguage, out var languageTables))
|
||||
{
|
||||
throw new LocalizationTableNotFoundException(tableName);
|
||||
}
|
||||
|
||||
if (!languageTables.TryGetValue(tableName, out var table))
|
||||
{
|
||||
throw new LocalizationTableNotFoundException(tableName);
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetText(string table, string key)
|
||||
{
|
||||
return GetTable(table).GetRawText(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILocalizationString GetString(string table, string key)
|
||||
{
|
||||
return new LocalizationString(this, table, key);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetText(string table, string key, out string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
text = GetText(table, key);
|
||||
return true;
|
||||
}
|
||||
catch (LocalizationException)
|
||||
{
|
||||
// 只捕获本地化相关的异常(键不存在、表不存在等)
|
||||
text = string.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterFormatter(string name, ILocalizationFormatter formatter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
_formatters[name] = formatter ?? throw new ArgumentNullException(nameof(formatter));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILocalizationFormatter? GetFormatter(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _formatters.TryGetValue(name, out var formatter) ? formatter : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SubscribeToLanguageChange(Action<string> callback)
|
||||
{
|
||||
if (callback == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(callback));
|
||||
}
|
||||
|
||||
if (!_languageChangeCallbacks.Contains(callback))
|
||||
{
|
||||
_languageChangeCallbacks.Add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void UnsubscribeFromLanguageChange(Action<string> callback)
|
||||
{
|
||||
if (callback == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(callback));
|
||||
}
|
||||
|
||||
_languageChangeCallbacks.Remove(callback);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 扫描可用语言
|
||||
ScanAvailableLanguages();
|
||||
|
||||
// 加载默认语言
|
||||
LoadLanguage(_config.DefaultLanguage);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
_tables.Clear();
|
||||
_formatters.Clear();
|
||||
_languageChangeCallbacks.Clear();
|
||||
}
|
||||
|
||||
private void RegisterBuiltInFormatters()
|
||||
{
|
||||
RegisterFormatter("if", new ConditionalFormatter());
|
||||
RegisterFormatter("plural", new PluralFormatter());
|
||||
RegisterFormatter("compact", new CompactNumberLocalizationFormatter());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 扫描可用语言
|
||||
/// </summary>
|
||||
private void ScanAvailableLanguages()
|
||||
{
|
||||
_availableLanguages.Clear();
|
||||
|
||||
var localizationPath = _config.LocalizationPath;
|
||||
if (!Directory.Exists(localizationPath))
|
||||
{
|
||||
_availableLanguages.Add(_config.DefaultLanguage);
|
||||
return;
|
||||
}
|
||||
|
||||
var directories = Directory.GetDirectories(localizationPath);
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
var languageCode = Path.GetFileName(dir);
|
||||
if (!string.IsNullOrEmpty(languageCode))
|
||||
{
|
||||
_availableLanguages.Add(languageCode);
|
||||
}
|
||||
}
|
||||
|
||||
if (_availableLanguages.Count == 0)
|
||||
{
|
||||
_availableLanguages.Add(_config.DefaultLanguage);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载语言
|
||||
/// </summary>
|
||||
private void LoadLanguage(string languageCode)
|
||||
{
|
||||
if (_tables.ContainsKey(languageCode))
|
||||
{
|
||||
return; // 已加载
|
||||
}
|
||||
|
||||
var languageTables = new Dictionary<string, ILocalizationTable>();
|
||||
|
||||
// 加载回退语言(如果不是默认语言)
|
||||
Dictionary<string, ILocalizationTable>? fallbackTables = null;
|
||||
if (languageCode != _config.FallbackLanguage)
|
||||
{
|
||||
LoadLanguage(_config.FallbackLanguage);
|
||||
_tables.TryGetValue(_config.FallbackLanguage, out fallbackTables);
|
||||
}
|
||||
|
||||
// 加载目标语言
|
||||
var languagePath = Path.Combine(_config.LocalizationPath, languageCode);
|
||||
if (Directory.Exists(languagePath))
|
||||
{
|
||||
var jsonFiles = Directory.GetFiles(languagePath, "*.json");
|
||||
foreach (var file in jsonFiles)
|
||||
{
|
||||
var tableName = Path.GetFileNameWithoutExtension(file);
|
||||
var data = LoadJsonFile(file);
|
||||
|
||||
ILocalizationTable? fallback = null;
|
||||
fallbackTables?.TryGetValue(tableName, out fallback);
|
||||
|
||||
languageTables[tableName] = new LocalizationTable(tableName, languageCode, data, fallback);
|
||||
}
|
||||
}
|
||||
|
||||
_tables[languageCode] = languageTables;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载 JSON 文件
|
||||
/// </summary>
|
||||
private static Dictionary<string, string> LoadJsonFile(string filePath)
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var data = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
return data ?? new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取文化信息
|
||||
/// </summary>
|
||||
private static CultureInfo GetCultureInfo(string languageCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 尝试映射常见的语言代码
|
||||
var cultureCode = languageCode switch
|
||||
{
|
||||
"eng" => "en-US",
|
||||
"zhs" => "zh-CN",
|
||||
"zht" => "zh-TW",
|
||||
"jpn" => "ja-JP",
|
||||
"kor" => "ko-KR",
|
||||
"fra" => "fr-FR",
|
||||
"deu" => "de-DE",
|
||||
"spa" => "es-ES",
|
||||
"rus" => "ru-RU",
|
||||
_ => languageCode
|
||||
};
|
||||
|
||||
return new CultureInfo(cultureCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return CultureInfo.InvariantCulture;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发语言变化事件
|
||||
/// </summary>
|
||||
private void TriggerLanguageChange()
|
||||
{
|
||||
foreach (var callback in _languageChangeCallbacks.ToList())
|
||||
{
|
||||
try
|
||||
{
|
||||
callback(_currentLanguage);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略回调异常
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
237
GFramework.Core/Localization/LocalizationString.cs
Normal file
237
GFramework.Core/Localization/LocalizationString.cs
Normal file
@ -0,0 +1,237 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using GFramework.Core.Abstractions.Localization;
|
||||
|
||||
namespace GFramework.Core.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化字符串实现
|
||||
/// </summary>
|
||||
public class LocalizationString : ILocalizationString
|
||||
{
|
||||
/// <summary>
|
||||
/// 匹配 {variableName} 或 {variableName:formatter:args} 的正则表达式模式
|
||||
/// </summary>
|
||||
private const string FormatVariablePattern =
|
||||
@"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]+))?)?\}";
|
||||
|
||||
/// <summary>
|
||||
/// 预编译的静态正则表达式,用于格式化字符串中的变量替换
|
||||
/// </summary>
|
||||
private static readonly Regex FormatVariableRegex =
|
||||
new(FormatVariablePattern, RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
private readonly ILocalizationManager _manager;
|
||||
private readonly Dictionary<string, object> _variables;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化本地化字符串
|
||||
/// </summary>
|
||||
/// <param name="manager">本地化管理器实例</param>
|
||||
/// <param name="table">本地化表名,用于定位本地化资源表</param>
|
||||
/// <param name="key">本地化键名,用于在表中定位具体的本地化文本</param>
|
||||
/// <exception cref="ArgumentNullException">当 manager、table 或 key 为 null 时抛出</exception>
|
||||
public LocalizationString(ILocalizationManager manager, string table, string key)
|
||||
{
|
||||
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
|
||||
Table = table ?? throw new ArgumentNullException(nameof(table));
|
||||
Key = key ?? throw new ArgumentNullException(nameof(key));
|
||||
_variables = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Table { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 添加单个变量到本地化字符串中
|
||||
/// </summary>
|
||||
/// <param name="name">变量名称,用于在模板中匹配对应的占位符</param>
|
||||
/// <param name="value">变量值,将被转换为字符串并替换到对应位置</param>
|
||||
/// <returns>返回当前的 LocalizationString 实例,支持链式调用</returns>
|
||||
/// <exception cref="ArgumentNullException">当 name 为 null 时抛出</exception>
|
||||
public ILocalizationString WithVariable(string name, object value)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
_variables[name] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量添加多个变量到本地化字符串中
|
||||
/// </summary>
|
||||
/// <param name="variables">变量元组数组,每个元组包含变量名称和对应的值</param>
|
||||
/// <returns>返回当前的 LocalizationString 实例,支持链式调用</returns>
|
||||
/// <exception cref="ArgumentNullException">当 variables 为 null 时抛出</exception>
|
||||
public ILocalizationString WithVariables(params (string name, object value)[] variables)
|
||||
{
|
||||
if (variables == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(variables));
|
||||
}
|
||||
|
||||
foreach (var (name, value) in variables)
|
||||
{
|
||||
WithVariable(name, value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化本地化字符串,将模板中的变量占位符替换为实际值
|
||||
/// </summary>
|
||||
/// <returns>格式化后的完整字符串。如果本地化文本不存在,则返回 "[Table.Key]" 格式的占位符</returns>
|
||||
/// <remarks>
|
||||
/// 支持两种格式:
|
||||
/// 1. {variableName} - 简单变量替换
|
||||
/// 2. {variableName:formatter:args} - 使用格式化器进行格式化
|
||||
/// </remarks>
|
||||
public string Format()
|
||||
{
|
||||
var rawText = GetRaw();
|
||||
return FormatString(rawText, _variables, _manager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取原始的本地化文本,不进行任何变量替换
|
||||
/// </summary>
|
||||
/// <returns>本地化文本。如果在本地化管理器中未找到对应的文本,则返回 "[Table.Key]" 格式的占位符</returns>
|
||||
public string GetRaw()
|
||||
{
|
||||
if (!_manager.TryGetText(Table, Key, out var text))
|
||||
{
|
||||
return $"[{Table}.{Key}]";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查当前本地化键是否存在于本地化管理器中
|
||||
/// </summary>
|
||||
/// <returns>如果存在返回 true;否则返回 false</returns>
|
||||
public bool Exists()
|
||||
{
|
||||
return _manager.TryGetText(Table, Key, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化字符串(支持变量替换和格式化器)
|
||||
/// </summary>
|
||||
/// <param name="template">包含占位符的模板字符串</param>
|
||||
/// <param name="variables">包含变量名称和值的字典</param>
|
||||
/// <param name="manager">本地化管理器实例,用于获取格式化器</param>
|
||||
/// <returns>格式化后的字符串。如果模板为空或 null,则直接返回原模板</returns>
|
||||
private static string FormatString(
|
||||
string template,
|
||||
Dictionary<string, object> variables,
|
||||
ILocalizationManager manager)
|
||||
{
|
||||
if (string.IsNullOrEmpty(template))
|
||||
{
|
||||
return template;
|
||||
}
|
||||
|
||||
// 使用预编译的静态正则表达式匹配 {variableName} 或 {variableName:formatter:args}
|
||||
return FormatVariableRegex.Replace(template, match => FormatMatch(match, variables, manager));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理单个正则表达式匹配项,根据是否有格式化器决定如何处理变量值
|
||||
/// </summary>
|
||||
/// <param name="match">正则表达式匹配结果</param>
|
||||
/// <param name="variables">变量字典</param>
|
||||
/// <param name="manager">本地化管理器实例</param>
|
||||
/// <returns>替换后的字符串。如果变量不存在则返回原始匹配值;如果有格式化器则尝试格式化,失败则使用默认格式化</returns>
|
||||
private static string FormatMatch(
|
||||
Match match,
|
||||
Dictionary<string, object> variables,
|
||||
ILocalizationManager manager)
|
||||
{
|
||||
var variableName = match.Groups[1].Value;
|
||||
if (!variables.TryGetValue(variableName, out var value))
|
||||
{
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
var formatterName = GetOptionalGroupValue(match, 2);
|
||||
if (string.IsNullOrEmpty(formatterName))
|
||||
{
|
||||
return FormatValue(value, manager);
|
||||
}
|
||||
|
||||
return TryFormatValue(match, value, formatterName, manager, out var result)
|
||||
? result
|
||||
: FormatValue(value, manager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试使用指定的格式化器格式化变量值
|
||||
/// </summary>
|
||||
/// <param name="match">正则表达式匹配结果,用于获取格式化参数</param>
|
||||
/// <param name="value">要格式化的变量值</param>
|
||||
/// <param name="formatterName">格式化器名称</param>
|
||||
/// <param name="manager">本地化管理器实例</param>
|
||||
/// <param name="result">格式化后的结果字符串</param>
|
||||
/// <returns>如果格式化成功返回 true;否则返回 false,此时 result 为空字符串</returns>
|
||||
private static bool TryFormatValue(
|
||||
Match match,
|
||||
object value,
|
||||
string formatterName,
|
||||
ILocalizationManager manager,
|
||||
out string result)
|
||||
{
|
||||
var formatterArgs = GetOptionalGroupValue(match, 3) ?? string.Empty;
|
||||
if (GetFormatter(manager, formatterName) is { } formatter &&
|
||||
formatter.TryFormat(formatterArgs, value, manager.CurrentCulture, out result))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
result = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对变量值进行默认格式化,不使用自定义格式化器
|
||||
/// </summary>
|
||||
/// <param name="value">要格式化的值</param>
|
||||
/// <param name="manager">本地化管理器实例,提供当前文化信息</param>
|
||||
/// <returns>格式化后的字符串。如果值实现 IFormattable 接口则使用其 ToString 方法,否则调用默认的 ToString 方法</returns>
|
||||
private static string FormatValue(object value, ILocalizationManager manager)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
IFormattable formattable => formattable.ToString(null, manager.CurrentCulture),
|
||||
_ => value.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取正则表达式匹配组中的可选值
|
||||
/// </summary>
|
||||
/// <param name="match">正则表达式匹配结果</param>
|
||||
/// <param name="groupIndex">要获取的组索引</param>
|
||||
/// <returns>如果该组匹配成功则返回其值;否则返回 null</returns>
|
||||
private static string? GetOptionalGroupValue(Match match, int groupIndex)
|
||||
{
|
||||
return match.Groups[groupIndex].Success ? match.Groups[groupIndex].Value : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从本地化管理器获取指定名称的格式化器
|
||||
/// </summary>
|
||||
/// <param name="manager">本地化管理器实例</param>
|
||||
/// <param name="name">格式化器名称</param>
|
||||
/// <returns>如果找到对应的格式化器则返回;否则返回 null</returns>
|
||||
private static ILocalizationFormatter? GetFormatter(ILocalizationManager manager, string name)
|
||||
{
|
||||
return manager.GetFormatter(name);
|
||||
}
|
||||
}
|
||||
136
GFramework.Core/Localization/LocalizationTable.cs
Normal file
136
GFramework.Core/Localization/LocalizationTable.cs
Normal file
@ -0,0 +1,136 @@
|
||||
using GFramework.Core.Abstractions.Localization;
|
||||
|
||||
namespace GFramework.Core.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化表实现
|
||||
/// </summary>
|
||||
public class LocalizationTable : ILocalizationTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 存储原始本地化数据的字典
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, string> _data;
|
||||
|
||||
/// <summary>
|
||||
/// 存储覆盖数据的字典,优先级高于原始数据
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, string> _overrides;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化本地化表
|
||||
/// </summary>
|
||||
/// <param name="name">表名</param>
|
||||
/// <param name="language">语言代码</param>
|
||||
/// <param name="data">数据字典</param>
|
||||
/// <param name="fallback">回退表</param>
|
||||
public LocalizationTable(
|
||||
string name,
|
||||
string language,
|
||||
IReadOnlyDictionary<string, string> data,
|
||||
ILocalizationTable? fallback = null)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Language = language ?? throw new ArgumentNullException(nameof(language));
|
||||
_data = new Dictionary<string, string>(data);
|
||||
_overrides = new Dictionary<string, string>();
|
||||
Fallback = fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化表的名称
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取语言代码
|
||||
/// </summary>
|
||||
public string Language { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取回退表,当当前表找不到键时用于查找
|
||||
/// </summary>
|
||||
public ILocalizationTable? Fallback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定键的原始文本内容
|
||||
/// </summary>
|
||||
/// <param name="key">要查找的本地化键</param>
|
||||
/// <returns>找到的本地化文本值</returns>
|
||||
/// <exception cref="ArgumentNullException">当 key 为 null 时抛出</exception>
|
||||
/// <exception cref="LocalizationKeyNotFoundException">当键在表中不存在且无回退表时抛出</exception>
|
||||
public string GetRawText(string key)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
// 优先使用覆盖数据
|
||||
if (_overrides.TryGetValue(key, out var overrideValue))
|
||||
{
|
||||
return overrideValue;
|
||||
}
|
||||
|
||||
// 然后使用原始数据
|
||||
if (_data.TryGetValue(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// 最后尝试回退表
|
||||
if (Fallback is { } fb && fb.ContainsKey(key))
|
||||
{
|
||||
return fb.GetRawText(key);
|
||||
}
|
||||
|
||||
throw new LocalizationKeyNotFoundException(Name, key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否包含指定的键
|
||||
/// </summary>
|
||||
/// <param name="key">要检查的本地化键</param>
|
||||
/// <returns>如果存在则返回 true,否则返回 false</returns>
|
||||
public bool ContainsKey(string key)
|
||||
{
|
||||
return _overrides.ContainsKey(key)
|
||||
|| _data.ContainsKey(key)
|
||||
|| (Fallback is { } fb && fb.ContainsKey(key));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有可用的本地化键集合
|
||||
/// </summary>
|
||||
/// <returns>包含所有键的可枚举集合</returns>
|
||||
public IEnumerable<string> GetKeys()
|
||||
{
|
||||
var keys = new HashSet<string>(_data.Keys);
|
||||
keys.UnionWith(_overrides.Keys);
|
||||
|
||||
if (Fallback != null)
|
||||
{
|
||||
keys.UnionWith(Fallback.GetKeys());
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合并覆盖数据到当前表
|
||||
/// </summary>
|
||||
/// <param name="overrides">要合并的覆盖数据字典</param>
|
||||
/// <exception cref="ArgumentNullException">当 overrides 为 null 时抛出</exception>
|
||||
public void Merge(IReadOnlyDictionary<string, string> overrides)
|
||||
{
|
||||
if (overrides == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(overrides));
|
||||
}
|
||||
|
||||
foreach (var (key, value) in overrides)
|
||||
{
|
||||
_overrides[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,26 @@
|
||||
using System.Threading.Channels;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
|
||||
namespace GFramework.Core.Logging.Appenders;
|
||||
|
||||
/// <summary>
|
||||
/// 异步日志输出器,使用 Channel 实现非阻塞日志写入
|
||||
/// 异步日志输出器,使用 <see cref="Channel" /> 将调用线程与慢速日志目标解耦。
|
||||
/// </summary>
|
||||
public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 该输出器在后台线程中顺序消费日志条目,因此调用方不会因为文件 IO 或其他慢速输出目标而阻塞。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 内部输出器抛出的异常不会重新抛回调用线程;如需观察后台处理失败,请在构造函数中提供
|
||||
/// <c>processingErrorHandler</c> 回调。
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class AsyncLogAppender : ILogAppender
|
||||
{
|
||||
private readonly Channel<LogEntry> _channel;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private readonly SemaphoreSlim _flushSemaphore = new(0, 1);
|
||||
private readonly ILogAppender _innerAppender;
|
||||
private readonly Action<Exception>? _processingErrorHandler;
|
||||
private readonly Task _processingTask;
|
||||
private bool _disposed;
|
||||
private volatile bool _flushRequested;
|
||||
@ -21,9 +30,17 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
||||
/// </summary>
|
||||
/// <param name="innerAppender">内部日志输出器</param>
|
||||
/// <param name="bufferSize">缓冲区大小(默认 10000)</param>
|
||||
public AsyncLogAppender(ILogAppender innerAppender, int bufferSize = 10000)
|
||||
/// <param name="processingErrorHandler">
|
||||
/// 后台处理日志时的错误回调。
|
||||
/// 默认值为 <see langword="null" />,表示吞掉内部异常以避免污染宿主标准错误输出。
|
||||
/// </param>
|
||||
public AsyncLogAppender(
|
||||
ILogAppender innerAppender,
|
||||
int bufferSize = 10000,
|
||||
Action<Exception>? processingErrorHandler = null)
|
||||
{
|
||||
_innerAppender = innerAppender ?? throw new ArgumentNullException(nameof(innerAppender));
|
||||
_processingErrorHandler = processingErrorHandler;
|
||||
|
||||
if (bufferSize <= 0)
|
||||
throw new ArgumentException("Buffer size must be greater than 0.", nameof(bufferSize));
|
||||
@ -138,7 +155,8 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台处理日志的异步方法
|
||||
/// 后台处理日志的异步方法。
|
||||
/// 该循环必须始终保持存活,因此所有内部异常都通过回调上报并被吞掉。
|
||||
/// </summary>
|
||||
private async Task ProcessLogsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@ -152,8 +170,8 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录内部错误到控制台(避免递归)
|
||||
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Error processing log entry: {ex.Message}");
|
||||
// 后台消费失败只通过显式回调暴露,避免测试宿主将 stderr 误判为测试告警。
|
||||
ReportProcessingError(ex);
|
||||
}
|
||||
|
||||
// 检查是否有刷新请求且通道已空
|
||||
@ -175,7 +193,7 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Fatal error in processing task: {ex}");
|
||||
ReportProcessingError(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -184,10 +202,37 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
|
||||
{
|
||||
_innerAppender.Flush();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 忽略刷新错误
|
||||
ReportProcessingError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。
|
||||
/// 取消相关异常表示关闭流程中的预期控制流,不应被视为后台处理失败。
|
||||
/// </summary>
|
||||
/// <param name="exception">后台处理中捕获到的异常。</param>
|
||||
private void ReportProcessingError(Exception exception)
|
||||
{
|
||||
if (exception is OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_processingErrorHandler is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_processingErrorHandler(exception);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 错误观察者只用于诊断,绝不能反向影响日志处理线程的生命周期。
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,7 +80,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
|
||||
// 触发事件
|
||||
try
|
||||
{
|
||||
OnPauseStateChanged?.Invoke(group, false);
|
||||
RaisePauseStateChanged(group, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -97,7 +97,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
|
||||
/// <summary>
|
||||
/// 暂停状态变化事件,当暂停状态发生改变时触发。
|
||||
/// </summary>
|
||||
public event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||
public event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 推入一个新的暂停请求到指定的暂停组中。
|
||||
@ -488,7 +488,18 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
OnPauseStateChanged?.Invoke(group, isPaused);
|
||||
RaisePauseStateChanged(group, isPaused);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以标准事件模式发布暂停状态变化事件。
|
||||
/// 所有状态变更路径都通过该方法创建统一的事件参数,避免不同调用点出现不一致的载荷。
|
||||
/// </summary>
|
||||
/// <param name="group">发生状态变化的暂停组。</param>
|
||||
/// <param name="isPaused">暂停组变化后的新状态。</param>
|
||||
private void RaisePauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
OnPauseStateChanged?.Invoke(this, new PauseStateChangedEventArgs(group, isPaused));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
58
GFramework.Core/Utility/Numeric/NumericDisplay.cs
Normal file
58
GFramework.Core/Utility/Numeric/NumericDisplay.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System.Numerics;
|
||||
using GFramework.Core.Abstractions.Utility.Numeric;
|
||||
|
||||
namespace GFramework.Core.Utility.Numeric;
|
||||
|
||||
/// <summary>
|
||||
/// 数值显示静态入口。
|
||||
/// </summary>
|
||||
public static class NumericDisplay
|
||||
{
|
||||
private static readonly NumericDisplayFormatter DefaultFormatter = new();
|
||||
|
||||
/// <summary>
|
||||
/// 将数值格式化为展示字符串。
|
||||
/// </summary>
|
||||
public static string Format<T>(T value, NumericFormatOptions? options = null) where T : INumber<T>
|
||||
{
|
||||
return DefaultFormatter.Format(value, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将运行时数值对象格式化为展示字符串。
|
||||
/// </summary>
|
||||
public static string Format(object value, NumericFormatOptions? options = null)
|
||||
{
|
||||
return DefaultFormatter.Format(value, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用默认紧凑风格格式化数值。
|
||||
/// </summary>
|
||||
public static string FormatCompact<T>(
|
||||
T value,
|
||||
int maxDecimalPlaces = 1,
|
||||
IFormatProvider? formatProvider = null) where T : INumber<T>
|
||||
{
|
||||
return Format(value, new NumericFormatOptions
|
||||
{
|
||||
MaxDecimalPlaces = maxDecimalPlaces,
|
||||
FormatProvider = formatProvider
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用默认紧凑风格格式化运行时数值对象。
|
||||
/// </summary>
|
||||
public static string FormatCompact(
|
||||
object value,
|
||||
int maxDecimalPlaces = 1,
|
||||
IFormatProvider? formatProvider = null)
|
||||
{
|
||||
return Format(value, new NumericFormatOptions
|
||||
{
|
||||
MaxDecimalPlaces = maxDecimalPlaces,
|
||||
FormatProvider = formatProvider
|
||||
});
|
||||
}
|
||||
}
|
||||
140
GFramework.Core/Utility/Numeric/NumericDisplayFormatter.cs
Normal file
140
GFramework.Core/Utility/Numeric/NumericDisplayFormatter.cs
Normal file
@ -0,0 +1,140 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using GFramework.Core.Abstractions.Utility.Numeric;
|
||||
|
||||
namespace GFramework.Core.Utility.Numeric;
|
||||
|
||||
/// <summary>
|
||||
/// 默认数值显示格式化器。
|
||||
/// </summary>
|
||||
public sealed class NumericDisplayFormatter : INumericDisplayFormatter
|
||||
{
|
||||
private readonly INumericFormatRule _defaultRule;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化默认数值显示格式化器。
|
||||
/// </summary>
|
||||
public NumericDisplayFormatter()
|
||||
: this(NumericSuffixFormatRule.InternationalCompact)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化数值显示格式化器。
|
||||
/// </summary>
|
||||
/// <param name="defaultRule">默认规则。</param>
|
||||
public NumericDisplayFormatter(INumericFormatRule defaultRule)
|
||||
{
|
||||
_defaultRule = defaultRule ?? throw new ArgumentNullException(nameof(defaultRule));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Format<T>(T value, NumericFormatOptions? options = null)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
var resolvedOptions = NormalizeOptions(options);
|
||||
var rule = ResolveRule(resolvedOptions);
|
||||
|
||||
if (rule.TryFormat(value, resolvedOptions, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return FormatFallback(value!, resolvedOptions.FormatProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将运行时数值对象格式化为展示字符串。
|
||||
/// </summary>
|
||||
/// <param name="value">待格式化的数值对象。</param>
|
||||
/// <param name="options">格式化选项。</param>
|
||||
/// <returns>格式化后的字符串。</returns>
|
||||
public string Format(object value, NumericFormatOptions? options = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
return value switch
|
||||
{
|
||||
byte byteValue => Format(byteValue, options),
|
||||
sbyte sbyteValue => Format(sbyteValue, options),
|
||||
short shortValue => Format(shortValue, options),
|
||||
ushort ushortValue => Format(ushortValue, options),
|
||||
int intValue => Format(intValue, options),
|
||||
uint uintValue => Format(uintValue, options),
|
||||
long longValue => Format(longValue, options),
|
||||
ulong ulongValue => Format(ulongValue, options),
|
||||
nint nativeIntValue => Format(nativeIntValue, options),
|
||||
nuint nativeUIntValue => Format(nativeUIntValue, options),
|
||||
float floatValue => Format(floatValue, options),
|
||||
double doubleValue => Format(doubleValue, options),
|
||||
decimal decimalValue => Format(decimalValue, options),
|
||||
BigInteger bigIntegerValue => Format(bigIntegerValue, options),
|
||||
_ => FormatFallback(value, options?.FormatProvider)
|
||||
};
|
||||
}
|
||||
|
||||
internal static NumericFormatOptions NormalizeOptions(NumericFormatOptions? options)
|
||||
{
|
||||
var resolved = options ?? new NumericFormatOptions();
|
||||
|
||||
if (resolved.MaxDecimalPlaces < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(options),
|
||||
resolved.MaxDecimalPlaces,
|
||||
"MaxDecimalPlaces 不能小于 0。");
|
||||
}
|
||||
|
||||
if (resolved.MinDecimalPlaces < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(options),
|
||||
resolved.MinDecimalPlaces,
|
||||
"MinDecimalPlaces 不能小于 0。");
|
||||
}
|
||||
|
||||
if (resolved.MinDecimalPlaces > resolved.MaxDecimalPlaces)
|
||||
{
|
||||
throw new ArgumentException("MinDecimalPlaces 不能大于 MaxDecimalPlaces。", nameof(options));
|
||||
}
|
||||
|
||||
if (resolved.CompactThreshold <= 0m)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(options),
|
||||
resolved.CompactThreshold,
|
||||
"CompactThreshold 必须大于 0。");
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private INumericFormatRule ResolveRule(NumericFormatOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
if (options.Rule is not null)
|
||||
{
|
||||
return options.Rule;
|
||||
}
|
||||
|
||||
return options.Style switch
|
||||
{
|
||||
NumericDisplayStyle.Compact => _defaultRule,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(options), options.Style, "不支持的数值显示风格。")
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatFallback(object value, IFormatProvider? provider)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
IFormattable formattable => formattable.ToString(null, provider ?? CultureInfo.CurrentCulture),
|
||||
_ => value.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
353
GFramework.Core/Utility/Numeric/NumericSuffixFormatRule.cs
Normal file
353
GFramework.Core/Utility/Numeric/NumericSuffixFormatRule.cs
Normal file
@ -0,0 +1,353 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using GFramework.Core.Abstractions.Utility.Numeric;
|
||||
|
||||
namespace GFramework.Core.Utility.Numeric;
|
||||
|
||||
/// <summary>
|
||||
/// 基于后缀阈值表的数值缩写规则。
|
||||
/// </summary>
|
||||
public sealed class NumericSuffixFormatRule : INumericFormatRule
|
||||
{
|
||||
private readonly NumericSuffixThreshold[] _thresholds;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化后缀缩写规则。
|
||||
/// </summary>
|
||||
/// <param name="name">规则名称。</param>
|
||||
/// <param name="thresholds">阈值表。</param>
|
||||
public NumericSuffixFormatRule(string name, IEnumerable<NumericSuffixThreshold> thresholds)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(name);
|
||||
ArgumentNullException.ThrowIfNull(thresholds);
|
||||
|
||||
Name = name;
|
||||
_thresholds = thresholds.OrderBy(entry => entry.Divisor).ToArray();
|
||||
|
||||
if (_thresholds.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("至少需要一个缩写阈值。", nameof(thresholds));
|
||||
}
|
||||
|
||||
ValidateThresholds(_thresholds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认国际缩写规则,使用标准的K、M、B、T后缀表示千、百万、十亿、万亿。
|
||||
/// </summary>
|
||||
public static NumericSuffixFormatRule InternationalCompact { get; } = new(
|
||||
"compact",
|
||||
[
|
||||
new NumericSuffixThreshold(1_000m, "K"),
|
||||
new NumericSuffixThreshold(1_000_000m, "M"),
|
||||
new NumericSuffixThreshold(1_000_000_000m, "B"),
|
||||
new NumericSuffixThreshold(1_000_000_000_000m, "T")
|
||||
]);
|
||||
|
||||
/// <summary>
|
||||
/// 获取此格式化规则的名称。
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将指定的数值按照当前规则进行格式化。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数值的类型</typeparam>
|
||||
/// <param name="value">要格式化的数值</param>
|
||||
/// <param name="options">格式化选项,包含小数位数、舍入模式等设置</param>
|
||||
/// <param name="result">格式化后的字符串结果</param>
|
||||
/// <returns>如果格式化成功则返回true;如果输入无效或格式化失败则返回false</returns>
|
||||
public bool TryFormat<T>(T value, NumericFormatOptions options, out string result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
NumericDisplayFormatter.NormalizeOptions(options);
|
||||
|
||||
if (TryFormatSpecialFloatingPoint(value, options.FormatProvider, out result))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
object? boxedValue = value;
|
||||
if (boxedValue is null)
|
||||
{
|
||||
result = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
return boxedValue switch
|
||||
{
|
||||
byte byteValue => TryFormatDecimal(byteValue, options, out result),
|
||||
sbyte sbyteValue => TryFormatDecimal(sbyteValue, options, out result),
|
||||
short shortValue => TryFormatDecimal(shortValue, options, out result),
|
||||
ushort ushortValue => TryFormatDecimal(ushortValue, options, out result),
|
||||
int intValue => TryFormatDecimal(intValue, options, out result),
|
||||
uint uintValue => TryFormatDecimal(uintValue, options, out result),
|
||||
long longValue => TryFormatDecimal(longValue, options, out result),
|
||||
ulong ulongValue => TryFormatDecimal(ulongValue, options, out result),
|
||||
nint nativeIntValue => TryFormatDecimal(nativeIntValue, options, out result),
|
||||
nuint nativeUIntValue => TryFormatDecimal(nativeUIntValue, options, out result),
|
||||
decimal decimalValue => TryFormatDecimal(decimalValue, options, out result),
|
||||
float floatValue => TryFormatDouble(floatValue, options, out result),
|
||||
double doubleValue => TryFormatDouble(doubleValue, options, out result),
|
||||
BigInteger bigIntegerValue => TryFormatBigInteger(bigIntegerValue, options, out result),
|
||||
_ => TryFormatConvertible(boxedValue, options, out result)
|
||||
};
|
||||
}
|
||||
|
||||
private static void ValidateThresholds(IReadOnlyList<NumericSuffixThreshold> thresholds)
|
||||
{
|
||||
decimal? previousDivisor = null;
|
||||
|
||||
foreach (var threshold in thresholds)
|
||||
{
|
||||
if (threshold.Divisor <= 0m)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(thresholds), "阈值除数必须大于 0。");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(threshold.Suffix))
|
||||
{
|
||||
throw new ArgumentException("阈值后缀不能为空。", nameof(thresholds));
|
||||
}
|
||||
|
||||
if (previousDivisor.HasValue && threshold.Divisor <= previousDivisor.Value)
|
||||
{
|
||||
throw new ArgumentException("阈值除数必须严格递增。", nameof(thresholds));
|
||||
}
|
||||
|
||||
previousDivisor = threshold.Divisor;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryFormatSpecialFloatingPoint<T>(
|
||||
T value,
|
||||
IFormatProvider? provider,
|
||||
out string result)
|
||||
{
|
||||
object? boxedValue = value;
|
||||
if (boxedValue is null)
|
||||
{
|
||||
result = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (boxedValue)
|
||||
{
|
||||
case float floatValue when float.IsNaN(floatValue) || float.IsInfinity(floatValue):
|
||||
result = floatValue.ToString(null, provider);
|
||||
return true;
|
||||
case double doubleValue when double.IsNaN(doubleValue) || double.IsInfinity(doubleValue):
|
||||
result = doubleValue.ToString(null, provider);
|
||||
return true;
|
||||
default:
|
||||
result = string.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFormatConvertible(object value, NumericFormatOptions options, out string result)
|
||||
{
|
||||
if (value is not IConvertible convertible)
|
||||
{
|
||||
result = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var decimalValue = convertible.ToDecimal(options.FormatProvider ?? CultureInfo.InvariantCulture);
|
||||
return TryFormatDecimal(decimalValue, options, out result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = string.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFormatBigInteger(BigInteger value, NumericFormatOptions options, out string result)
|
||||
{
|
||||
try
|
||||
{
|
||||
return TryFormatDecimal((decimal)value, options, out result);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
var doubleValue = (double)value;
|
||||
if (TryFormatSpecialFloatingPoint(doubleValue, options.FormatProvider, out result))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return TryFormatDouble(doubleValue, options, out result);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFormatDecimal(decimal value, NumericFormatOptions options, out string result)
|
||||
{
|
||||
var absoluteValue = Math.Abs(value);
|
||||
|
||||
if (absoluteValue < options.CompactThreshold)
|
||||
{
|
||||
result = FormatPlainDecimal(value, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
var suffixIndex = FindThresholdIndex(absoluteValue);
|
||||
if (suffixIndex < 0)
|
||||
{
|
||||
result = FormatPlainDecimal(value, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
var scaledValue = RoundScaledDecimal(absoluteValue, suffixIndex, options, out suffixIndex);
|
||||
result = ComposeResult(value < 0m, FormatDecimalCore(scaledValue, options, false), suffixIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryFormatDouble(double value, NumericFormatOptions options, out string result)
|
||||
{
|
||||
var absoluteValue = Math.Abs(value);
|
||||
|
||||
if (absoluteValue < (double)options.CompactThreshold)
|
||||
{
|
||||
result = FormatPlainDouble(value, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
var suffixIndex = FindThresholdIndex(absoluteValue);
|
||||
if (suffixIndex < 0)
|
||||
{
|
||||
result = FormatPlainDouble(value, options);
|
||||
return true;
|
||||
}
|
||||
|
||||
var scaledValue = RoundScaledDouble(absoluteValue, suffixIndex, options, out suffixIndex);
|
||||
result = ComposeResult(value < 0d, FormatDoubleCore(scaledValue, options, false), suffixIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
private string ComposeResult(bool negative, string numericPart, int suffixIndex)
|
||||
{
|
||||
return $"{(negative ? "-" : string.Empty)}{numericPart}{_thresholds[suffixIndex].Suffix}";
|
||||
}
|
||||
|
||||
private int FindThresholdIndex(decimal absoluteValue)
|
||||
{
|
||||
for (var i = _thresholds.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (absoluteValue >= _thresholds[i].Divisor)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int FindThresholdIndex(double absoluteValue)
|
||||
{
|
||||
for (var i = _thresholds.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (absoluteValue >= (double)_thresholds[i].Divisor)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private decimal RoundScaledDecimal(decimal absoluteValue, int suffixIndex, NumericFormatOptions options,
|
||||
out int resolvedIndex)
|
||||
{
|
||||
resolvedIndex = suffixIndex;
|
||||
var roundedValue = RoundDecimal(absoluteValue / _thresholds[resolvedIndex].Divisor, options);
|
||||
|
||||
while (resolvedIndex < _thresholds.Length - 1)
|
||||
{
|
||||
var promoteThreshold = _thresholds[resolvedIndex + 1].Divisor / _thresholds[resolvedIndex].Divisor;
|
||||
if (roundedValue < promoteThreshold)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
resolvedIndex++;
|
||||
roundedValue = RoundDecimal(absoluteValue / _thresholds[resolvedIndex].Divisor, options);
|
||||
}
|
||||
|
||||
return roundedValue;
|
||||
}
|
||||
|
||||
private double RoundScaledDouble(double absoluteValue, int suffixIndex, NumericFormatOptions options,
|
||||
out int resolvedIndex)
|
||||
{
|
||||
resolvedIndex = suffixIndex;
|
||||
var roundedValue = RoundDouble(absoluteValue / (double)_thresholds[resolvedIndex].Divisor, options);
|
||||
|
||||
while (resolvedIndex < _thresholds.Length - 1)
|
||||
{
|
||||
var promoteThreshold =
|
||||
(double)(_thresholds[resolvedIndex + 1].Divisor / _thresholds[resolvedIndex].Divisor);
|
||||
if (roundedValue < promoteThreshold)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
resolvedIndex++;
|
||||
roundedValue = RoundDouble(absoluteValue / (double)_thresholds[resolvedIndex].Divisor, options);
|
||||
}
|
||||
|
||||
return roundedValue;
|
||||
}
|
||||
|
||||
private static decimal RoundDecimal(decimal value, NumericFormatOptions options)
|
||||
{
|
||||
return Math.Round(value, options.MaxDecimalPlaces, options.MidpointRounding);
|
||||
}
|
||||
|
||||
private static double RoundDouble(double value, NumericFormatOptions options)
|
||||
{
|
||||
return Math.Round(value, options.MaxDecimalPlaces, options.MidpointRounding);
|
||||
}
|
||||
|
||||
private static string FormatPlainDecimal(decimal value, NumericFormatOptions options)
|
||||
{
|
||||
return FormatDecimalCore(RoundDecimal(value, options), options, options.UseGroupingBelowThreshold);
|
||||
}
|
||||
|
||||
private static string FormatPlainDouble(double value, NumericFormatOptions options)
|
||||
{
|
||||
return FormatDoubleCore(RoundDouble(value, options), options, options.UseGroupingBelowThreshold);
|
||||
}
|
||||
|
||||
private static string FormatDecimalCore(decimal value, NumericFormatOptions options, bool useGrouping)
|
||||
{
|
||||
return value.ToString(BuildFormatString(options, useGrouping), options.FormatProvider);
|
||||
}
|
||||
|
||||
private static string FormatDoubleCore(double value, NumericFormatOptions options, bool useGrouping)
|
||||
{
|
||||
return value.ToString(BuildFormatString(options, useGrouping), options.FormatProvider);
|
||||
}
|
||||
|
||||
private static string BuildFormatString(NumericFormatOptions options, bool useGrouping)
|
||||
{
|
||||
var integerPart = useGrouping ? "#,0" : "0";
|
||||
|
||||
if (options.MaxDecimalPlaces == 0)
|
||||
{
|
||||
return integerPart;
|
||||
}
|
||||
|
||||
if (!options.TrimTrailingZeros)
|
||||
{
|
||||
var fixedDigits = Math.Max(options.MaxDecimalPlaces, options.MinDecimalPlaces);
|
||||
return $"{integerPart}.{new string('0', fixedDigits)}";
|
||||
}
|
||||
|
||||
var requiredDigits = new string('0', options.MinDecimalPlaces);
|
||||
var optionalDigits = new string('#', options.MaxDecimalPlaces - options.MinDecimalPlaces);
|
||||
return $"{integerPart}.{requiredDigits}{optionalDigits}";
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
|
||||
<TestTargetFrameworks Condition="'$(TestTargetFrameworks)' == ''">net10.0</TestTargetFrameworks>
|
||||
<TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<Using Include="GFramework.Game.Abstractions"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.19">
|
||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.25">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
25
GFramework.Game.Abstractions/Routing/IRoute.cs
Normal file
25
GFramework.Game.Abstractions/Routing/IRoute.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2026 GeWuYou
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
namespace GFramework.Game.Abstractions.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// 路由项接口,表示可路由的对象
|
||||
/// </summary>
|
||||
public interface IRoute
|
||||
{
|
||||
/// <summary>
|
||||
/// 路由键值,用于唯一标识路由项
|
||||
/// </summary>
|
||||
string Key { get; }
|
||||
}
|
||||
26
GFramework.Game.Abstractions/Routing/IRouteContext.cs
Normal file
26
GFramework.Game.Abstractions/Routing/IRouteContext.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2026 GeWuYou
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
namespace GFramework.Game.Abstractions.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// 路由上下文接口,表示路由进入时的参数
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 这是一个标记接口,用于类型约束。
|
||||
/// 具体的路由上下文类型应该实现此接口。
|
||||
/// </remarks>
|
||||
public interface IRouteContext
|
||||
{
|
||||
// 标记接口,用于类型约束
|
||||
}
|
||||
54
GFramework.Game.Abstractions/Routing/IRouteGuard.cs
Normal file
54
GFramework.Game.Abstractions/Routing/IRouteGuard.cs
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2026 GeWuYou
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
namespace GFramework.Game.Abstractions.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// 路由守卫接口,用于控制路由的进入和离开
|
||||
/// </summary>
|
||||
/// <typeparam name="TRoute">路由项类型</typeparam>
|
||||
public interface IRouteGuard<TRoute> where TRoute : IRoute
|
||||
{
|
||||
/// <summary>
|
||||
/// 守卫优先级,数值越小优先级越高
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 守卫按优先级从小到大依次执行。
|
||||
/// 建议使用 0-100 的范围,默认为 50。
|
||||
/// </remarks>
|
||||
int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可以中断后续守卫的执行
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 如果为 true,当此守卫返回 true 或抛出异常时,将中断后续守卫的执行。
|
||||
/// 如果为 false,将继续执行后续守卫。
|
||||
/// </remarks>
|
||||
bool CanInterrupt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以进入指定路由
|
||||
/// </summary>
|
||||
/// <param name="routeKey">路由键值</param>
|
||||
/// <param name="context">路由上下文</param>
|
||||
/// <returns>如果允许进入返回 true,否则返回 false</returns>
|
||||
ValueTask<bool> CanEnterAsync(string routeKey, IRouteContext? context);
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以离开指定路由
|
||||
/// </summary>
|
||||
/// <param name="routeKey">路由键值</param>
|
||||
/// <returns>如果允许离开返回 true,否则返回 false</returns>
|
||||
ValueTask<bool> CanLeaveAsync(string routeKey);
|
||||
}
|
||||
@ -2,29 +2,25 @@
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using GFramework.Game.Abstractions.Routing;
|
||||
|
||||
namespace GFramework.Game.Abstractions.Scene;
|
||||
|
||||
/// <summary>
|
||||
/// 场景行为接口,定义了场景生命周期管理的标准方法。
|
||||
/// 实现此接口的类需要处理场景的加载、激活、暂停、恢复和卸载等核心操作。
|
||||
/// </summary>
|
||||
public interface ISceneBehavior
|
||||
public interface ISceneBehavior : IRoute
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取场景的唯一标识符。
|
||||
/// 用于区分不同的场景实例。
|
||||
/// </summary>
|
||||
string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取场景的原始对象。
|
||||
/// </summary>
|
||||
|
||||
@ -2,19 +2,21 @@
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using GFramework.Game.Abstractions.Routing;
|
||||
|
||||
namespace GFramework.Game.Abstractions.Scene;
|
||||
|
||||
/// <summary>
|
||||
/// 场景进入参数接口
|
||||
/// 该接口用于定义场景跳转时传递的参数数据结构
|
||||
/// 场景进入参数接口
|
||||
/// 该接口用于定义场景跳转时传递的参数数据结构
|
||||
/// </summary>
|
||||
public interface ISceneEnterParam;
|
||||
public interface ISceneEnterParam : IRouteContext;
|
||||
@ -11,40 +11,29 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using GFramework.Game.Abstractions.Routing;
|
||||
|
||||
namespace GFramework.Game.Abstractions.Scene;
|
||||
|
||||
/// <summary>
|
||||
/// 场景路由守卫接口,用于在场景切换前进行权限检查和条件验证。
|
||||
/// 实现此接口可以拦截场景的进入和离开操作。
|
||||
/// </summary>
|
||||
public interface ISceneRouteGuard
|
||||
public interface ISceneRouteGuard : IRouteGuard<ISceneBehavior>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取守卫的执行优先级。
|
||||
/// 数值越小优先级越高,越先执行。
|
||||
/// 建议范围:-1000 到 1000。
|
||||
/// </summary>
|
||||
int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取守卫是否可以中断后续守卫的执行。
|
||||
/// true 表示当前守卫通过后,可以跳过后续守卫直接允许操作。
|
||||
/// false 表示即使当前守卫通过,仍需执行所有后续守卫。
|
||||
/// </summary>
|
||||
bool CanInterrupt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 异步检查是否允许进入指定场景。
|
||||
/// </summary>
|
||||
/// <param name="sceneKey">目标场景的唯一标识符。</param>
|
||||
/// <param name="param">场景进入参数,可能包含初始化数据或上下文信息。</param>
|
||||
/// <returns>如果允许进入则返回 true,否则返回 false。</returns>
|
||||
Task<bool> CanEnterAsync(string sceneKey, ISceneEnterParam? param);
|
||||
ValueTask<bool> CanEnterAsync(string sceneKey, ISceneEnterParam? param);
|
||||
|
||||
/// <summary>
|
||||
/// 异步检查是否允许离开指定场景。
|
||||
/// 该成员显式细化了通用路由守卫的离开检查,使场景守卫在 API 文档中保持场景语义。
|
||||
/// </summary>
|
||||
/// <param name="sceneKey">当前场景的唯一标识符。</param>
|
||||
/// <returns>如果允许离开则返回 true,否则返回 false。</returns>
|
||||
Task<bool> CanLeaveAsync(string sceneKey);
|
||||
new ValueTask<bool> CanLeaveAsync(string sceneKey);
|
||||
}
|
||||
@ -1,11 +1,12 @@
|
||||
using GFramework.Game.Abstractions.Enums;
|
||||
using GFramework.Game.Abstractions.Routing;
|
||||
|
||||
namespace GFramework.Game.Abstractions.UI;
|
||||
|
||||
/// <summary>
|
||||
/// UI页面行为接口,定义了UI页面的生命周期方法和状态管理
|
||||
/// </summary>
|
||||
public interface IUiPageBehavior
|
||||
public interface IUiPageBehavior : IRoute
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置当前UI句柄。
|
||||
@ -45,14 +46,6 @@ public interface IUiPageBehavior
|
||||
/// <returns>页面视图实例。</returns>
|
||||
object View { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取键值
|
||||
/// </summary>
|
||||
/// <value>返回当前对象的键标识符</value>
|
||||
string Key { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取页面是否处于活动状态
|
||||
/// </summary>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
namespace GFramework.Game.Abstractions.UI;
|
||||
using GFramework.Game.Abstractions.Routing;
|
||||
|
||||
namespace GFramework.Game.Abstractions.UI;
|
||||
|
||||
/// <summary>
|
||||
/// UI页面进入参数接口
|
||||
/// 该接口用于定义UI页面跳转时传递的参数数据结构
|
||||
/// UI页面进入参数接口
|
||||
/// 该接口用于定义UI页面跳转时传递的参数数据结构
|
||||
/// </summary>
|
||||
public interface IUiPageEnterParam;
|
||||
public interface IUiPageEnterParam : IRouteContext;
|
||||
@ -1,34 +1,26 @@
|
||||
using GFramework.Game.Abstractions.Routing;
|
||||
|
||||
namespace GFramework.Game.Abstractions.UI;
|
||||
|
||||
/// <summary>
|
||||
/// UI路由守卫接口
|
||||
/// 用于拦截和处理UI路由切换,实现业务逻辑解耦
|
||||
/// UI路由守卫接口
|
||||
/// 用于拦截和处理UI路由切换,实现业务逻辑解耦
|
||||
/// </summary>
|
||||
public interface IUiRouteGuard
|
||||
public interface IUiRouteGuard : IRouteGuard<IUiPageBehavior>
|
||||
{
|
||||
/// <summary>
|
||||
/// 守卫优先级,数值越小越先执行
|
||||
/// </summary>
|
||||
int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可中断后续守卫
|
||||
/// 如果返回 true,当该守卫返回 false 时,将停止执行后续守卫
|
||||
/// </summary>
|
||||
bool CanInterrupt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 进入UI前的检查
|
||||
/// 进入UI前的检查
|
||||
/// </summary>
|
||||
/// <param name="uiKey">目标UI标识符</param>
|
||||
/// <param name="param">进入参数</param>
|
||||
/// <returns>true表示允许进入,false表示拦截</returns>
|
||||
Task<bool> CanEnterAsync(string uiKey, IUiPageEnterParam? param);
|
||||
ValueTask<bool> CanEnterAsync(string uiKey, IUiPageEnterParam? param);
|
||||
|
||||
/// <summary>
|
||||
/// 离开UI前的检查
|
||||
/// 离开UI前的检查。
|
||||
/// 该成员显式细化了通用路由守卫的离开检查,使 UI 守卫在 API 文档中保持 UI 语义。
|
||||
/// </summary>
|
||||
/// <param name="uiKey">当前UI标识符</param>
|
||||
/// <returns>true表示允许离开,false表示拦截</returns>
|
||||
Task<bool> CanLeaveAsync(string uiKey);
|
||||
new ValueTask<bool> CanLeaveAsync(string uiKey);
|
||||
}
|
||||
@ -113,28 +113,6 @@ public interface IUiRouter : ISystem
|
||||
/// </summary>
|
||||
bool Contains(string uiKey);
|
||||
|
||||
#region 路由守卫
|
||||
|
||||
/// <summary>
|
||||
/// 注册路由守卫
|
||||
/// </summary>
|
||||
/// <param name="guard">守卫实例</param>
|
||||
void AddGuard(IUiRouteGuard guard);
|
||||
|
||||
/// <summary>
|
||||
/// 注册路由守卫(泛型方法)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">守卫类型,必须实现 IUiRouteGuard 且有无参构造函数</typeparam>
|
||||
void AddGuard<T>() where T : IUiRouteGuard, new();
|
||||
|
||||
/// <summary>
|
||||
/// 移除路由守卫
|
||||
/// </summary>
|
||||
/// <param name="guard">守卫实例</param>
|
||||
void RemoveGuard(IUiRouteGuard guard);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Layer UI
|
||||
|
||||
/// <summary>
|
||||
|
||||
24
GFramework.Game.Tests/GFramework.Game.Tests.csproj
Normal file
24
GFramework.Game.Tests/GFramework.Game.Tests.csproj
Normal file
@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TestTargetFrameworks Condition="'$(TestTargetFrameworks)' == ''">net10.0</TestTargetFrameworks>
|
||||
<TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0"/>
|
||||
<PackageReference Include="Moq" Version="4.20.72"/>
|
||||
<PackageReference Include="NUnit" Version="4.5.1"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GFramework.Game\GFramework.Game.csproj"/>
|
||||
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
19
GFramework.Game.Tests/GlobalUsings.cs
Normal file
19
GFramework.Game.Tests/GlobalUsings.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2026 GeWuYou
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
global using NUnit.Framework;
|
||||
global using Moq;
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading.Tasks;
|
||||
582
GFramework.Game.Tests/Routing/RouterBaseTests.cs
Normal file
582
GFramework.Game.Tests/Routing/RouterBaseTests.cs
Normal file
@ -0,0 +1,582 @@
|
||||
// Copyright (c) 2026 GeWuYou
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using GFramework.Game.Abstractions.Routing;
|
||||
using GFramework.Game.Routing;
|
||||
|
||||
namespace GFramework.Game.Tests.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// RouterBase 单元测试
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class RouterBaseTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试用路由项
|
||||
/// </summary>
|
||||
private class TestRoute : IRoute
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试用路由上下文
|
||||
/// </summary>
|
||||
private class TestContext : IRouteContext
|
||||
{
|
||||
public string? Data { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试用路由守卫
|
||||
/// </summary>
|
||||
private class TestGuard : IRouteGuard<TestRoute>
|
||||
{
|
||||
public Func<string, IRouteContext?, ValueTask<bool>>? EnterFunc { get; set; }
|
||||
public Func<string, ValueTask<bool>>? LeaveFunc { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public bool CanInterrupt { get; set; }
|
||||
|
||||
public ValueTask<bool> CanEnterAsync(string routeKey, IRouteContext? context)
|
||||
{
|
||||
return EnterFunc?.Invoke(routeKey, context) ?? ValueTask.FromResult(true);
|
||||
}
|
||||
|
||||
public ValueTask<bool> CanLeaveAsync(string routeKey)
|
||||
{
|
||||
return LeaveFunc?.Invoke(routeKey) ?? ValueTask.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试用路由器实现
|
||||
/// </summary>
|
||||
private class TestRouter : RouterBase<TestRoute, TestContext>
|
||||
{
|
||||
public bool HandlersRegistered { get; private set; }
|
||||
|
||||
// 暴露 Stack 用于测试
|
||||
public new Stack<TestRoute> Stack => base.Stack;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 测试用路由器不需要初始化逻辑
|
||||
}
|
||||
|
||||
protected override void RegisterHandlers()
|
||||
{
|
||||
HandlersRegistered = true;
|
||||
}
|
||||
|
||||
// 暴露 protected 方法用于测试
|
||||
public new Task<bool> ExecuteEnterGuardsAsync(string routeKey, TestContext? context)
|
||||
{
|
||||
return base.ExecuteEnterGuardsAsync(routeKey, context);
|
||||
}
|
||||
|
||||
public new Task<bool> ExecuteLeaveGuardsAsync(string routeKey)
|
||||
{
|
||||
return base.ExecuteLeaveGuardsAsync(routeKey);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddGuard_ShouldAddGuardToList()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var guard = new TestGuard { Priority = 10 };
|
||||
|
||||
// Act
|
||||
router.AddGuard(guard);
|
||||
|
||||
// Assert - 通过尝试添加相同守卫来验证
|
||||
Assert.DoesNotThrow(() => router.AddGuard(guard));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddGuard_ShouldSortByPriority()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var guard1 = new TestGuard { Priority = 20 };
|
||||
var guard2 = new TestGuard { Priority = 10 };
|
||||
var guard3 = new TestGuard { Priority = 30 };
|
||||
|
||||
// Act
|
||||
router.AddGuard(guard1);
|
||||
router.AddGuard(guard2);
|
||||
router.AddGuard(guard3);
|
||||
|
||||
// Assert - 通过执行守卫来验证顺序
|
||||
var executionOrder = new List<int>();
|
||||
guard1.EnterFunc = (_, _) =>
|
||||
{
|
||||
executionOrder.Add(1);
|
||||
return ValueTask.FromResult(true);
|
||||
};
|
||||
guard2.EnterFunc = (_, _) =>
|
||||
{
|
||||
executionOrder.Add(2);
|
||||
return ValueTask.FromResult(true);
|
||||
};
|
||||
guard3.EnterFunc = (_, _) =>
|
||||
{
|
||||
executionOrder.Add(3);
|
||||
return ValueTask.FromResult(true);
|
||||
};
|
||||
|
||||
router.ExecuteEnterGuardsAsync("test", null).Wait();
|
||||
|
||||
Assert.That(executionOrder, Is.EqualTo(new[] { 2, 1, 3 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddGuard_WithGeneric_ShouldCreateAndAddGuard()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act & Assert
|
||||
Assert.DoesNotThrow(() => router.AddGuard<TestGuard>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddGuard_WithNull_ShouldThrowArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => router.AddGuard(null!));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RemoveGuard_ShouldRemoveGuardFromList()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var guard = new TestGuard { Priority = 10 };
|
||||
router.AddGuard(guard);
|
||||
|
||||
// Act
|
||||
router.RemoveGuard(guard);
|
||||
|
||||
// Assert - 守卫应该被移除,不会再执行
|
||||
var executed = false;
|
||||
guard.EnterFunc = (_, _) =>
|
||||
{
|
||||
executed = true;
|
||||
return ValueTask.FromResult(true);
|
||||
};
|
||||
|
||||
router.ExecuteEnterGuardsAsync("test", null).Wait();
|
||||
|
||||
Assert.That(executed, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RemoveGuard_WithNull_ShouldThrowArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => router.RemoveGuard(null!));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecuteEnterGuardsAsync_WithNoGuards_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act
|
||||
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecuteEnterGuardsAsync_WithAllowingGuard_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var guard = new TestGuard
|
||||
{
|
||||
Priority = 10,
|
||||
EnterFunc = (_, _) => ValueTask.FromResult(true)
|
||||
};
|
||||
router.AddGuard(guard);
|
||||
|
||||
// Act
|
||||
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecuteEnterGuardsAsync_WithBlockingGuard_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var guard = new TestGuard
|
||||
{
|
||||
Priority = 10,
|
||||
EnterFunc = (_, _) => ValueTask.FromResult(false)
|
||||
};
|
||||
router.AddGuard(guard);
|
||||
|
||||
// Act
|
||||
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecuteEnterGuardsAsync_WithInterruptingGuard_ShouldStopExecution()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var guard1 = new TestGuard
|
||||
{
|
||||
Priority = 10,
|
||||
CanInterrupt = true,
|
||||
EnterFunc = (_, _) => ValueTask.FromResult(true)
|
||||
};
|
||||
var guard2Executed = false;
|
||||
var guard2 = new TestGuard
|
||||
{
|
||||
Priority = 20,
|
||||
EnterFunc = (_, _) =>
|
||||
{
|
||||
guard2Executed = true;
|
||||
return ValueTask.FromResult(true);
|
||||
}
|
||||
};
|
||||
router.AddGuard(guard1);
|
||||
router.AddGuard(guard2);
|
||||
|
||||
// Act
|
||||
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
Assert.That(guard2Executed, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecuteEnterGuardsAsync_WithThrowingGuard_ShouldContinueIfNotInterrupting()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var guard1 = new TestGuard
|
||||
{
|
||||
Priority = 10,
|
||||
CanInterrupt = false,
|
||||
EnterFunc = (_, _) => throw new InvalidOperationException("Test exception")
|
||||
};
|
||||
var guard2Executed = false;
|
||||
var guard2 = new TestGuard
|
||||
{
|
||||
Priority = 20,
|
||||
EnterFunc = (_, _) =>
|
||||
{
|
||||
guard2Executed = true;
|
||||
return ValueTask.FromResult(true);
|
||||
}
|
||||
};
|
||||
router.AddGuard(guard1);
|
||||
router.AddGuard(guard2);
|
||||
|
||||
// Act
|
||||
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
Assert.That(guard2Executed, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecuteEnterGuardsAsync_WithThrowingInterruptingGuard_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var guard = new TestGuard
|
||||
{
|
||||
Priority = 10,
|
||||
CanInterrupt = true,
|
||||
EnterFunc = (_, _) => throw new InvalidOperationException("Test exception")
|
||||
};
|
||||
router.AddGuard(guard);
|
||||
|
||||
// Act
|
||||
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecuteLeaveGuardsAsync_WithNoGuards_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act
|
||||
var result = await router.ExecuteLeaveGuardsAsync("test");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecuteLeaveGuardsAsync_WithAllowingGuard_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var guard = new TestGuard
|
||||
{
|
||||
Priority = 10,
|
||||
LeaveFunc = _ => ValueTask.FromResult(true)
|
||||
};
|
||||
router.AddGuard(guard);
|
||||
|
||||
// Act
|
||||
var result = await router.ExecuteLeaveGuardsAsync("test");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ExecuteLeaveGuardsAsync_WithBlockingGuard_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var guard = new TestGuard
|
||||
{
|
||||
Priority = 10,
|
||||
LeaveFunc = _ => ValueTask.FromResult(false)
|
||||
};
|
||||
router.AddGuard(guard);
|
||||
|
||||
// Act
|
||||
var result = await router.ExecuteLeaveGuardsAsync("test");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Contains_WithEmptyStack_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act
|
||||
var result = router.Contains("test");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Contains_WithMatchingRoute_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var route = new TestRoute { Key = "test" };
|
||||
router.Stack.Push(route);
|
||||
|
||||
// Act
|
||||
var result = router.Contains("test");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Contains_WithNonMatchingRoute_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var route = new TestRoute { Key = "test1" };
|
||||
router.Stack.Push(route);
|
||||
|
||||
// Act
|
||||
var result = router.Contains("test2");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PeekKey_WithEmptyStack_ShouldReturnEmptyString()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act
|
||||
var result = router.PeekKey();
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo(string.Empty));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PeekKey_WithRoute_ShouldReturnRouteKey()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var route = new TestRoute { Key = "test" };
|
||||
router.Stack.Push(route);
|
||||
|
||||
// Act
|
||||
var result = router.PeekKey();
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo("test"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsTop_WithEmptyStack_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act
|
||||
var result = router.IsTop("test");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsTop_WithMatchingRoute_ShouldReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var route = new TestRoute { Key = "test" };
|
||||
router.Stack.Push(route);
|
||||
|
||||
// Act
|
||||
var result = router.IsTop("test");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsTop_WithNonMatchingRoute_ShouldReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var route = new TestRoute { Key = "test1" };
|
||||
router.Stack.Push(route);
|
||||
|
||||
// Act
|
||||
var result = router.IsTop("test2");
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Current_WithEmptyStack_ShouldReturnNull()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act
|
||||
var result = router.Current;
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Current_WithRoute_ShouldReturnTopRoute()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var route = new TestRoute { Key = "test" };
|
||||
router.Stack.Push(route);
|
||||
|
||||
// Act
|
||||
var result = router.Current;
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo(route));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CurrentKey_WithEmptyStack_ShouldReturnNull()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act
|
||||
var result = router.CurrentKey;
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CurrentKey_WithRoute_ShouldReturnRouteKey()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
var route = new TestRoute { Key = "test" };
|
||||
router.Stack.Push(route);
|
||||
|
||||
// Act
|
||||
var result = router.CurrentKey;
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo("test"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Count_WithEmptyStack_ShouldReturnZero()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
|
||||
// Act
|
||||
var result = router.Count;
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Count_WithRoutes_ShouldReturnCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
var router = new TestRouter();
|
||||
router.Stack.Push(new TestRoute { Key = "test1" });
|
||||
router.Stack.Push(new TestRoute { Key = "test2" });
|
||||
|
||||
// Act
|
||||
var result = router.Count;
|
||||
|
||||
// Assert
|
||||
Assert.That(result, Is.EqualTo(2));
|
||||
}
|
||||
}
|
||||
244
GFramework.Game/Routing/RouterBase.cs
Normal file
244
GFramework.Game/Routing/RouterBase.cs
Normal file
@ -0,0 +1,244 @@
|
||||
// Copyright (c) 2026 GeWuYou
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Systems;
|
||||
using GFramework.Game.Abstractions.Routing;
|
||||
|
||||
namespace GFramework.Game.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// 路由器基类,提供通用的路由管理功能
|
||||
/// </summary>
|
||||
/// <typeparam name="TRoute">路由项类型,必须实现 IRoute 接口</typeparam>
|
||||
/// <typeparam name="TContext">路由上下文类型,必须实现 IRouteContext 接口</typeparam>
|
||||
/// <remarks>
|
||||
/// 此基类提供了以下通用功能:
|
||||
/// - 路由守卫管理 (AddGuard/RemoveGuard)
|
||||
/// - 守卫执行逻辑 (ExecuteEnterGuardsAsync/ExecuteLeaveGuardsAsync)
|
||||
/// - 路由栈管理 (Stack/Current/CurrentKey)
|
||||
/// - 栈操作方法 (Contains/PeekKey/IsTop)
|
||||
/// </remarks>
|
||||
public abstract class RouterBase<TRoute, TContext> : AbstractSystem
|
||||
where TRoute : IRoute
|
||||
where TContext : IRouteContext
|
||||
{
|
||||
private static readonly ILogger Log =
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(RouterBase<TRoute, TContext>));
|
||||
|
||||
/// <summary>
|
||||
/// 路由守卫列表,按优先级排序
|
||||
/// </summary>
|
||||
private readonly List<IRouteGuard<TRoute>> _guards = new();
|
||||
|
||||
/// <summary>
|
||||
/// 路由栈,用于管理路由的显示顺序和导航历史
|
||||
/// </summary>
|
||||
protected readonly Stack<TRoute> Stack = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前路由 (栈顶元素)
|
||||
/// </summary>
|
||||
public TRoute? Current => Stack.Count > 0 ? Stack.Peek() : default;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前路由的键值
|
||||
/// </summary>
|
||||
public string? CurrentKey => Current?.Key;
|
||||
|
||||
/// <summary>
|
||||
/// 获取栈深度
|
||||
/// </summary>
|
||||
public int Count => Stack.Count;
|
||||
|
||||
#region Abstract Methods
|
||||
|
||||
/// <summary>
|
||||
/// 注册过渡处理器 (由子类实现)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 子类应该在此方法中注册所有需要的过渡处理器。
|
||||
/// 此方法在 OnInit 中被调用。
|
||||
/// </remarks>
|
||||
protected abstract void RegisterHandlers();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Guard Management
|
||||
|
||||
/// <summary>
|
||||
/// 添加路由守卫
|
||||
/// </summary>
|
||||
/// <param name="guard">路由守卫实例</param>
|
||||
/// <exception cref="ArgumentNullException">当守卫实例为 null 时抛出</exception>
|
||||
public void AddGuard(IRouteGuard<TRoute> guard)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(guard);
|
||||
|
||||
if (_guards.Contains(guard))
|
||||
{
|
||||
Log.Debug("Guard already registered: {0}", guard.GetType().Name);
|
||||
return;
|
||||
}
|
||||
|
||||
_guards.Add(guard);
|
||||
_guards.Sort((a, b) => a.Priority.CompareTo(b.Priority));
|
||||
Log.Debug("Guard registered: {0}, Priority={1}", guard.GetType().Name, guard.Priority);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加路由守卫 (泛型版本)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">守卫类型,必须实现 IRouteGuard 接口且有无参构造函数</typeparam>
|
||||
public void AddGuard<T>() where T : IRouteGuard<TRoute>, new()
|
||||
{
|
||||
AddGuard(new T());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除路由守卫
|
||||
/// </summary>
|
||||
/// <param name="guard">要移除的路由守卫实例</param>
|
||||
/// <exception cref="ArgumentNullException">当守卫实例为 null 时抛出</exception>
|
||||
public void RemoveGuard(IRouteGuard<TRoute> guard)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(guard);
|
||||
if (_guards.Remove(guard))
|
||||
Log.Debug("Guard removed: {0}", guard.GetType().Name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Guard Execution
|
||||
|
||||
/// <summary>
|
||||
/// 执行进入守卫检查
|
||||
/// </summary>
|
||||
/// <param name="routeKey">路由键值</param>
|
||||
/// <param name="context">路由上下文</param>
|
||||
/// <returns>如果所有守卫都允许进入返回 true,否则返回 false</returns>
|
||||
/// <remarks>
|
||||
/// 守卫按优先级从小到大依次执行。
|
||||
/// 如果某个守卫返回 false 且 CanInterrupt 为 true,则中断后续守卫的执行。
|
||||
/// 如果某个守卫抛出异常且 CanInterrupt 为 true,则中断后续守卫的执行。
|
||||
/// </remarks>
|
||||
protected async Task<bool> ExecuteEnterGuardsAsync(string routeKey, TContext? context)
|
||||
{
|
||||
foreach (var guard in _guards)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, routeKey);
|
||||
var canEnter = await guard.CanEnterAsync(routeKey, context);
|
||||
|
||||
if (!canEnter)
|
||||
{
|
||||
Log.Debug("Enter guard blocked: {0}", guard.GetType().Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (guard.CanInterrupt)
|
||||
{
|
||||
Log.Debug("Enter guard {0} passed, can interrupt = true", guard.GetType().Name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Enter guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
||||
if (guard.CanInterrupt)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行离开守卫检查
|
||||
/// </summary>
|
||||
/// <param name="routeKey">路由键值</param>
|
||||
/// <returns>如果所有守卫都允许离开返回 true,否则返回 false</returns>
|
||||
/// <remarks>
|
||||
/// 守卫按优先级从小到大依次执行。
|
||||
/// 如果某个守卫返回 false 且 CanInterrupt 为 true,则中断后续守卫的执行。
|
||||
/// 如果某个守卫抛出异常且 CanInterrupt 为 true,则中断后续守卫的执行。
|
||||
/// </remarks>
|
||||
protected async Task<bool> ExecuteLeaveGuardsAsync(string routeKey)
|
||||
{
|
||||
foreach (var guard in _guards)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, routeKey);
|
||||
var canLeave = await guard.CanLeaveAsync(routeKey);
|
||||
|
||||
if (!canLeave)
|
||||
{
|
||||
Log.Debug("Leave guard blocked: {0}", guard.GetType().Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (guard.CanInterrupt)
|
||||
{
|
||||
Log.Debug("Leave guard {0} passed, can interrupt = true", guard.GetType().Name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Leave guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
||||
if (guard.CanInterrupt)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stack Operations
|
||||
|
||||
/// <summary>
|
||||
/// 检查栈中是否包含指定路由
|
||||
/// </summary>
|
||||
/// <param name="routeKey">路由键值</param>
|
||||
/// <returns>如果栈中包含指定路由返回 true,否则返回 false</returns>
|
||||
public bool Contains(string routeKey)
|
||||
{
|
||||
return Stack.Any(r => r.Key == routeKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取栈顶路由的键值
|
||||
/// </summary>
|
||||
/// <returns>栈顶路由的键值,如果栈为空则返回空字符串</returns>
|
||||
public string PeekKey()
|
||||
{
|
||||
return Stack.Count == 0 ? string.Empty : Stack.Peek().Key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断栈顶是否为指定路由
|
||||
/// </summary>
|
||||
/// <param name="routeKey">路由键值</param>
|
||||
/// <returns>如果栈顶是指定路由返回 true,否则返回 false</returns>
|
||||
public bool IsTop(string routeKey)
|
||||
{
|
||||
return Stack.Count != 0 && Stack.Peek().Key.Equals(routeKey);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -14,9 +14,9 @@
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Extensions;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Systems;
|
||||
using GFramework.Game.Abstractions.Enums;
|
||||
using GFramework.Game.Abstractions.Scene;
|
||||
using GFramework.Game.Routing;
|
||||
|
||||
namespace GFramework.Game.Scene;
|
||||
|
||||
@ -25,15 +25,13 @@ namespace GFramework.Game.Scene;
|
||||
/// 实现了 <see cref="ISceneRouter"/> 接口,用于管理场景的加载、替换和卸载操作。
|
||||
/// </summary>
|
||||
public abstract class SceneRouterBase
|
||||
: AbstractSystem, ISceneRouter
|
||||
: RouterBase<ISceneBehavior, ISceneEnterParam>, ISceneRouter
|
||||
{
|
||||
private static readonly ILogger Log =
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(SceneRouterBase));
|
||||
|
||||
private readonly List<ISceneRouteGuard> _guards = new();
|
||||
private readonly SceneTransitionPipeline _pipeline = new();
|
||||
|
||||
private readonly Stack<ISceneBehavior> _stack = new();
|
||||
private readonly SemaphoreSlim _transitionLock = new(1, 1);
|
||||
private ISceneFactory _factory = null!;
|
||||
|
||||
@ -45,17 +43,17 @@ public abstract class SceneRouterBase
|
||||
/// <summary>
|
||||
/// 获取当前场景行为对象。
|
||||
/// </summary>
|
||||
public ISceneBehavior? Current => _stack.Count > 0 ? _stack.Peek() : null;
|
||||
public new ISceneBehavior? Current => Stack.Count > 0 ? Stack.Peek() : null;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前场景的键名。
|
||||
/// </summary>
|
||||
public string? CurrentKey => Current?.Key;
|
||||
public new string? CurrentKey => Current?.Key;
|
||||
|
||||
/// <summary>
|
||||
/// 获取场景栈的只读视图,按压入顺序排列(从栈底到栈顶)。
|
||||
/// </summary>
|
||||
public IEnumerable<ISceneBehavior> Stack => _stack.Reverse();
|
||||
IEnumerable<ISceneBehavior> ISceneRouter.Stack => base.Stack.Reverse();
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否正在进行场景转换。
|
||||
@ -115,9 +113,9 @@ public abstract class SceneRouterBase
|
||||
/// </summary>
|
||||
/// <param name="sceneKey">场景键名。</param>
|
||||
/// <returns>如果场景在栈中返回true,否则返回false。</returns>
|
||||
public bool Contains(string sceneKey)
|
||||
public new bool Contains(string sceneKey)
|
||||
{
|
||||
return _stack.Any(s => s.Key == sceneKey);
|
||||
return Stack.Any(s => s.Key == sceneKey);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -163,46 +161,10 @@ public abstract class SceneRouterBase
|
||||
_pipeline.UnregisterAroundHandler(handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加场景路由守卫。
|
||||
/// </summary>
|
||||
/// <param name="guard">守卫实例。</param>
|
||||
public void AddGuard(ISceneRouteGuard guard)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(guard);
|
||||
if (!_guards.Contains(guard))
|
||||
{
|
||||
_guards.Add(guard);
|
||||
_guards.Sort((a, b) => a.Priority.CompareTo(b.Priority));
|
||||
Log.Debug("Guard added: {0}, Priority={1}", guard.GetType().Name, guard.Priority);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加场景路由守卫(泛型版本)。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">守卫类型。</typeparam>
|
||||
public void AddGuard<T>() where T : ISceneRouteGuard, new()
|
||||
{
|
||||
AddGuard(new T());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除场景路由守卫。
|
||||
/// </summary>
|
||||
/// <param name="guard">守卫实例。</param>
|
||||
public void RemoveGuard(ISceneRouteGuard guard)
|
||||
{
|
||||
if (_guards.Remove(guard))
|
||||
{
|
||||
Log.Debug("Guard removed: {0}", guard.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册场景过渡处理器的抽象方法,由子类实现。
|
||||
/// </summary>
|
||||
protected abstract void RegisterHandlers();
|
||||
protected override abstract void RegisterHandlers();
|
||||
|
||||
/// <summary>
|
||||
/// 系统初始化方法,获取场景工厂并注册处理器。
|
||||
@ -281,20 +243,20 @@ public abstract class SceneRouterBase
|
||||
await scene.OnLoadAsync(param);
|
||||
|
||||
// 暂停当前场景
|
||||
if (_stack.Count > 0)
|
||||
if (Stack.Count > 0)
|
||||
{
|
||||
var current = _stack.Peek();
|
||||
var current = Stack.Peek();
|
||||
await current.OnPauseAsync();
|
||||
}
|
||||
|
||||
// 压入栈
|
||||
_stack.Push(scene);
|
||||
Stack.Push(scene);
|
||||
|
||||
// 进入场景
|
||||
await scene.OnEnterAsync();
|
||||
|
||||
Log.Debug("Push Scene: {0}, stackCount={1}",
|
||||
sceneKey, _stack.Count);
|
||||
sceneKey, Stack.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -335,10 +297,10 @@ public abstract class SceneRouterBase
|
||||
/// <returns>异步任务。</returns>
|
||||
private async ValueTask PopInternalAsync()
|
||||
{
|
||||
if (_stack.Count == 0)
|
||||
if (Stack.Count == 0)
|
||||
return;
|
||||
|
||||
var top = _stack.Peek();
|
||||
var top = Stack.Peek();
|
||||
|
||||
// 守卫检查
|
||||
if (!await ExecuteLeaveGuardsAsync(top.Key))
|
||||
@ -347,7 +309,7 @@ public abstract class SceneRouterBase
|
||||
return;
|
||||
}
|
||||
|
||||
_stack.Pop();
|
||||
Stack.Pop();
|
||||
|
||||
// 退出场景
|
||||
await top.OnExitAsync();
|
||||
@ -359,13 +321,13 @@ public abstract class SceneRouterBase
|
||||
Root!.RemoveScene(top);
|
||||
|
||||
// 恢复下一个场景
|
||||
if (_stack.Count > 0)
|
||||
if (Stack.Count > 0)
|
||||
{
|
||||
var next = _stack.Peek();
|
||||
var next = Stack.Peek();
|
||||
await next.OnResumeAsync();
|
||||
}
|
||||
|
||||
Log.Debug("Pop Scene, stackCount={0}", _stack.Count);
|
||||
Log.Debug("Pop Scene, stackCount={0}", Stack.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -406,7 +368,7 @@ public abstract class SceneRouterBase
|
||||
/// <returns>异步任务。</returns>
|
||||
private async ValueTask ClearInternalAsync()
|
||||
{
|
||||
while (_stack.Count > 0)
|
||||
while (Stack.Count > 0)
|
||||
{
|
||||
await PopInternalAsync();
|
||||
}
|
||||
@ -460,82 +422,5 @@ public abstract class SceneRouterBase
|
||||
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行进入场景的守卫检查。
|
||||
/// 按优先级顺序执行所有守卫的CanEnterAsync方法。
|
||||
/// </summary>
|
||||
/// <param name="sceneKey">场景键名。</param>
|
||||
/// <param name="param">进入参数。</param>
|
||||
/// <returns>如果所有守卫都允许进入返回true,否则返回false。</returns>
|
||||
private async Task<bool> ExecuteEnterGuardsAsync(string sceneKey, ISceneEnterParam? param)
|
||||
{
|
||||
foreach (var guard in _guards)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, sceneKey);
|
||||
var canEnter = await guard.CanEnterAsync(sceneKey, param);
|
||||
|
||||
if (!canEnter)
|
||||
{
|
||||
Log.Debug("Enter guard blocked: {0}", guard.GetType().Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (guard.CanInterrupt)
|
||||
{
|
||||
Log.Debug("Enter guard {0} passed, can interrupt = true", guard.GetType().Name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Enter guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
||||
if (guard.CanInterrupt)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行离开场景的守卫检查。
|
||||
/// 按优先级顺序执行所有守卫的CanLeaveAsync方法。
|
||||
/// </summary>
|
||||
/// <param name="sceneKey">场景键名。</param>
|
||||
/// <returns>如果所有守卫都允许离开返回true,否则返回false。</returns>
|
||||
private async Task<bool> ExecuteLeaveGuardsAsync(string sceneKey)
|
||||
{
|
||||
foreach (var guard in _guards)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, sceneKey);
|
||||
var canLeave = await guard.CanLeaveAsync(sceneKey);
|
||||
|
||||
if (!canLeave)
|
||||
{
|
||||
Log.Debug("Leave guard blocked: {0}", guard.GetType().Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (guard.CanInterrupt)
|
||||
{
|
||||
Log.Debug("Leave guard {0} passed, can interrupt = true", guard.GetType().Name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Leave guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
||||
if (guard.CanInterrupt)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Extensions;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Core.Systems;
|
||||
using GFramework.Game.Abstractions.Enums;
|
||||
using GFramework.Game.Abstractions.UI;
|
||||
using GFramework.Game.Routing;
|
||||
|
||||
namespace GFramework.Game.UI;
|
||||
|
||||
@ -11,15 +11,10 @@ namespace GFramework.Game.UI;
|
||||
/// UI路由基类,提供页面栈管理和层级UI管理功能
|
||||
/// 负责UI页面的导航、显示、隐藏以及生命周期管理
|
||||
/// </summary>
|
||||
public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterParam>, IUiRouter
|
||||
{
|
||||
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(UiRouterBase));
|
||||
|
||||
/// <summary>
|
||||
/// 路由守卫列表,用于控制UI页面的进入和离开
|
||||
/// </summary>
|
||||
private readonly List<IUiRouteGuard> _guards = new();
|
||||
|
||||
/// <summary>
|
||||
/// 层级管理字典(非栈层级),用于管理Overlay、Modal、Toast等浮层UI
|
||||
/// Key: UiLayer枚举值, Value: InstanceId到PageBehavior的映射字典
|
||||
@ -31,11 +26,6 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
/// </summary>
|
||||
private readonly UiTransitionPipeline _pipeline = new();
|
||||
|
||||
/// <summary>
|
||||
/// 页面栈,用于管理UI页面的显示顺序和导航历史
|
||||
/// </summary>
|
||||
private readonly Stack<IUiPageBehavior> _stack = new();
|
||||
|
||||
/// <summary>
|
||||
/// UI工厂实例,用于创建UI页面和相关对象
|
||||
/// </summary>
|
||||
@ -98,7 +88,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
}
|
||||
|
||||
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
|
||||
Log.Debug("Push UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, _stack.Count);
|
||||
Log.Debug("Push UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, Stack.Count);
|
||||
|
||||
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
||||
{
|
||||
@ -126,7 +116,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
}
|
||||
|
||||
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
|
||||
Log.Debug("Push existing UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, _stack.Count);
|
||||
Log.Debug("Push existing UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, Stack.Count);
|
||||
|
||||
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
||||
{
|
||||
@ -142,13 +132,13 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
/// <param name="policy">页面弹出策略</param>
|
||||
public async ValueTask PopAsync(UiPopPolicy policy = UiPopPolicy.Destroy)
|
||||
{
|
||||
if (_stack.Count == 0)
|
||||
if (Stack.Count == 0)
|
||||
{
|
||||
Log.Debug("Pop ignored: stack is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
var leavingUiKey = _stack.Peek().Key;
|
||||
var leavingUiKey = Stack.Peek().Key;
|
||||
|
||||
if (!await ExecuteLeaveGuardsAsync(leavingUiKey))
|
||||
{
|
||||
@ -156,7 +146,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
return;
|
||||
}
|
||||
|
||||
var nextUiKey = _stack.Count > 1 ? _stack.ElementAt(1).Key : null;
|
||||
var nextUiKey = Stack.Count > 1 ? Stack.ElementAt(1).Key : null;
|
||||
var @event = CreateEvent(nextUiKey, UiTransitionType.Pop);
|
||||
|
||||
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
||||
@ -226,7 +216,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
public async ValueTask ClearAsync()
|
||||
{
|
||||
var @event = CreateEvent(string.Empty, UiTransitionType.Clear);
|
||||
Log.Debug("Clear UI Stack, stackCount={0}", _stack.Count);
|
||||
Log.Debug("Clear UI Stack, stackCount={0}", Stack.Count);
|
||||
|
||||
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
||||
{
|
||||
@ -240,9 +230,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
/// 获取栈顶元素的键值
|
||||
/// </summary>
|
||||
/// <returns>栈顶UI页面的键值,如果栈为空则返回空字符串</returns>
|
||||
public string PeekKey()
|
||||
public new string PeekKey()
|
||||
{
|
||||
return _stack.Count == 0 ? string.Empty : _stack.Peek().Key;
|
||||
return Stack.Count == 0 ? string.Empty : Stack.Peek().Key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -251,7 +241,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
/// <returns>栈顶UI页面行为实例,如果栈为空则返回null</returns>
|
||||
public IUiPageBehavior? Peek()
|
||||
{
|
||||
return _stack.Count == 0 ? null : _stack.Peek();
|
||||
return Stack.Count == 0 ? null : Stack.Peek();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -259,9 +249,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
/// </summary>
|
||||
/// <param name="uiKey">要检查的UI页面键值</param>
|
||||
/// <returns>如果栈顶是指定UI则返回true,否则返回false</returns>
|
||||
public bool IsTop(string uiKey)
|
||||
public new bool IsTop(string uiKey)
|
||||
{
|
||||
return _stack.Count != 0 && _stack.Peek().Key.Equals(uiKey);
|
||||
return Stack.Count != 0 && Stack.Peek().Key.Equals(uiKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -269,15 +259,15 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
/// </summary>
|
||||
/// <param name="uiKey">要检查的UI页面键值</param>
|
||||
/// <returns>如果栈中包含指定UI则返回true,否则返回false</returns>
|
||||
public bool Contains(string uiKey)
|
||||
public new bool Contains(string uiKey)
|
||||
{
|
||||
return _stack.Any(p => p.Key.Equals(uiKey));
|
||||
return Stack.Any(p => p.Key.Equals(uiKey));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取栈深度
|
||||
/// </summary>
|
||||
public int Count => _stack.Count;
|
||||
public new int Count => Stack.Count;
|
||||
|
||||
#endregion
|
||||
|
||||
@ -458,51 +448,6 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
|
||||
#endregion
|
||||
|
||||
#region Route Guards
|
||||
|
||||
/// <summary>
|
||||
/// 注册路由守卫
|
||||
/// </summary>
|
||||
/// <param name="guard">路由守卫实例</param>
|
||||
/// <exception cref="ArgumentNullException">当守卫实例为null时抛出</exception>
|
||||
public void AddGuard(IUiRouteGuard guard)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(guard);
|
||||
|
||||
if (_guards.Contains(guard))
|
||||
{
|
||||
Log.Debug("Guard already registered: {0}", guard.GetType().Name);
|
||||
return;
|
||||
}
|
||||
|
||||
_guards.Add(guard);
|
||||
_guards.Sort((a, b) => a.Priority.CompareTo(b.Priority));
|
||||
Log.Debug("Guard registered: {0}, Priority={1}", guard.GetType().Name, guard.Priority);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册路由守卫(泛型)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">路由守卫类型,必须实现IUiRouteGuard接口且有无参构造函数</typeparam>
|
||||
public void AddGuard<T>() where T : IUiRouteGuard, new()
|
||||
{
|
||||
AddGuard(new T());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除路由守卫
|
||||
/// </summary>
|
||||
/// <param name="guard">要移除的路由守卫实例</param>
|
||||
/// <exception cref="ArgumentNullException">当守卫实例为null时抛出</exception>
|
||||
public void RemoveGuard(IUiRouteGuard guard)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(guard);
|
||||
if (_guards.Remove(guard))
|
||||
Log.Debug("Guard removed: {0}", guard.GetType().Name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
/// <summary>
|
||||
@ -525,7 +470,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
/// 抽象方法,用于注册具体的处理程序。
|
||||
/// 子类必须实现此方法以完成特定的处理逻辑注册。
|
||||
/// </summary>
|
||||
protected abstract void RegisterHandlers();
|
||||
protected override abstract void RegisterHandlers();
|
||||
|
||||
#endregion
|
||||
|
||||
@ -655,9 +600,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
/// <param name="policy">过渡策略</param>
|
||||
private void DoPushPageInternal(IUiPageBehavior page, IUiPageEnterParam? param, UiTransitionPolicy policy)
|
||||
{
|
||||
if (_stack.Count > 0)
|
||||
if (Stack.Count > 0)
|
||||
{
|
||||
var current = _stack.Peek();
|
||||
var current = Stack.Peek();
|
||||
Log.Debug("Pause current page: {0}", current.View.GetType().Name);
|
||||
current.OnPause();
|
||||
|
||||
@ -671,9 +616,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
Log.Debug("Add page to UiRoot: {0}", page.View.GetType().Name);
|
||||
_uiRoot.AddUiPage(page);
|
||||
|
||||
_stack.Push(page);
|
||||
Stack.Push(page);
|
||||
|
||||
Log.Debug("Enter & Show page: {0}, stackAfter={1}", page.View.GetType().Name, _stack.Count);
|
||||
Log.Debug("Enter & Show page: {0}, stackAfter={1}", page.View.GetType().Name, Stack.Count);
|
||||
page.OnEnter(param);
|
||||
page.OnShow();
|
||||
}
|
||||
@ -684,12 +629,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
/// <param name="policy">页面弹出策略</param>
|
||||
private void DoPopInternal(UiPopPolicy policy)
|
||||
{
|
||||
if (_stack.Count == 0)
|
||||
if (Stack.Count == 0)
|
||||
return;
|
||||
|
||||
var top = _stack.Pop();
|
||||
var top = Stack.Pop();
|
||||
Log.Debug("Pop UI Page internal: {0}, policy={1}, stackAfterPop={2}",
|
||||
top.GetType().Name, policy, _stack.Count);
|
||||
top.GetType().Name, policy, Stack.Count);
|
||||
|
||||
if (policy == UiPopPolicy.Destroy)
|
||||
{
|
||||
@ -701,9 +646,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
top.OnHide();
|
||||
}
|
||||
|
||||
if (_stack.Count > 0)
|
||||
if (Stack.Count > 0)
|
||||
{
|
||||
var next = _stack.Peek();
|
||||
var next = Stack.Peek();
|
||||
next.OnResume();
|
||||
next.OnShow();
|
||||
}
|
||||
@ -715,85 +660,10 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
||||
/// <param name="policy">页面弹出策略</param>
|
||||
private void DoClearInternal(UiPopPolicy policy)
|
||||
{
|
||||
Log.Debug("Clear UI Stack internal, count={0}", _stack.Count);
|
||||
while (_stack.Count > 0)
|
||||
Log.Debug("Clear UI Stack internal, count={0}", Stack.Count);
|
||||
while (Stack.Count > 0)
|
||||
DoPopInternal(policy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行进入守卫检查
|
||||
/// </summary>
|
||||
/// <param name="uiKey">UI页面键值</param>
|
||||
/// <param name="param">页面进入参数</param>
|
||||
/// <returns>如果允许进入则返回true,否则返回false</returns>
|
||||
private async Task<bool> ExecuteEnterGuardsAsync(string uiKey, IUiPageEnterParam? param)
|
||||
{
|
||||
foreach (var guard in _guards)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, uiKey);
|
||||
var canEnter = await guard.CanEnterAsync(uiKey, param);
|
||||
|
||||
if (!canEnter)
|
||||
{
|
||||
Log.Debug("Enter guard blocked: {0}", guard.GetType().Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (guard.CanInterrupt)
|
||||
{
|
||||
Log.Debug("Enter guard {0} passed, can interrupt = true", guard.GetType().Name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Enter guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
||||
if (guard.CanInterrupt)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行离开守卫检查
|
||||
/// </summary>
|
||||
/// <param name="uiKey">UI页面键值</param>
|
||||
/// <returns>如果允许离开则返回true,否则返回false</returns>
|
||||
private async Task<bool> ExecuteLeaveGuardsAsync(string uiKey)
|
||||
{
|
||||
foreach (var guard in _guards)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, uiKey);
|
||||
var canLeave = await guard.CanLeaveAsync(uiKey);
|
||||
|
||||
if (!canLeave)
|
||||
{
|
||||
Log.Debug("Leave guard blocked: {0}", guard.GetType().Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (guard.CanInterrupt)
|
||||
{
|
||||
Log.Debug("Leave guard {0} passed, can interrupt = true", guard.GetType().Name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Leave guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
||||
if (guard.CanInterrupt)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<!-- import parent: https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<!--
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<Folder Include="logging\"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.19">
|
||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.25">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>GeWuYou.$(AssemblyName)</PackageId>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
|
||||
<!-- 这是 Analyzer,不是运行时库 -->
|
||||
<IsRoslynAnalyzer>true</IsRoslynAnalyzer>
|
||||
|
||||
@ -6,6 +6,9 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- Godot.SourceGenerators expects this property from Godot.NET.Sdk.
|
||||
Provide a safe default so source generators can run in plain SDK-style builds as well. -->
|
||||
<GodotProjectDir Condition="'$(GodotProjectDir)' == ''">$(MSBuildProjectDirectory)</GodotProjectDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
namespace GFramework.SourceGenerators.Abstractions.Bases;
|
||||
|
||||
/// <summary>
|
||||
/// 标记类的优先级,自动生成 <see cref="GFramework.Core.Abstractions.Bases.IPrioritized"/> 接口实现
|
||||
/// 标记类的优先级,自动生成 <c>GFramework.Core.Abstractions.Bases.IPrioritized</c> 接口实现。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 使用此特性可以避免手动实现 IPrioritized 接口。
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<!-- import parent: https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<!--
|
||||
|
||||
@ -15,10 +15,7 @@
|
||||
<Using Include="GFramework.SourceGenerators.Abstractions"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.19">
|
||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.25">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<!-- import parent: https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<!--
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all"/>
|
||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.19">
|
||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.25">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
|
||||
<TestTargetFrameworks Condition="'$(TestTargetFrameworks)' == ''">net10.0</TestTargetFrameworks>
|
||||
<TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
@ -56,7 +56,7 @@ public sealed class PriorityGenerator : MetadataAttributeClassGeneratorBase
|
||||
}
|
||||
|
||||
// 3. 必须是 partial
|
||||
if (syntax.Modifiers.All(m => m.Kind() != SyntaxKind.PartialKeyword))
|
||||
if (syntax.Modifiers.All(m => !m.IsKind(SyntaxKind.PartialKeyword)))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
PriorityDiagnostic.MustBePartial,
|
||||
|
||||
@ -19,7 +19,7 @@ internal static class PriorityDiagnostic
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Priority 特性设计用于类级别的优先级标记,不支持其他类型。"
|
||||
description: "Priority 特性设计用于类级别的优先级标记,不支持其他类型."
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@ -32,7 +32,7 @@ internal static class PriorityDiagnostic
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
description: "当类已经手动实现 IPrioritized 接口时,源生成器将跳过代码生成以避免冲突。"
|
||||
description: "当类已经手动实现 IPrioritized 接口时,源生成器将跳过代码生成以避免冲突."
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@ -45,7 +45,7 @@ internal static class PriorityDiagnostic
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "源生成器需要在 partial 类中生成 IPrioritized 接口实现。"
|
||||
description: "源生成器需要在 partial 类中生成 IPrioritized 接口实现."
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@ -58,7 +58,7 @@ internal static class PriorityDiagnostic
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Priority 特性必须提供一个有效的整数值。"
|
||||
description: "Priority 特性必须提供一个有效的整数值."
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@ -71,7 +71,7 @@ internal static class PriorityDiagnostic
|
||||
category: Category,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "Priority 特性仅支持顶层类,不支持嵌套类。请将嵌套类移至命名空间级别。"
|
||||
description: "Priority 特性仅支持顶层类,不支持嵌套类.请将嵌套类移至命名空间级别."
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@ -84,6 +84,6 @@ internal static class PriorityDiagnostic
|
||||
category: "GFramework.Usage",
|
||||
defaultSeverity: DiagnosticSeverity.Info,
|
||||
isEnabledByDefault: true,
|
||||
description: "当获取实现了 IPrioritized 接口的服务时,应使用 GetAllByPriority 方法以确保按优先级排序。"
|
||||
description: "当获取实现了 IPrioritized 接口的服务时,应使用 GetAllByPriority 方法以确保按优先级排序."
|
||||
);
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>GeWuYou.$(AssemblyName)</PackageId>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
|
||||
<!-- 这是 Analyzer,不是运行时库 -->
|
||||
<IsRoslynAnalyzer>true</IsRoslynAnalyzer>
|
||||
@ -30,7 +30,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj" PrivateAssets="all"/>
|
||||
<ProjectReference Include="..\$(AssemblyName).Common\$(AssemblyName).Common.csproj" PrivateAssets="all"/>
|
||||
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- ★关键:只把 Generator DLL 放进 analyzers -->
|
||||
|
||||
@ -16,11 +16,16 @@
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<!-- This package is a pure meta-package that only aggregates dependencies. -->
|
||||
<NoPackageAnalysis>false</NoPackageAnalysis>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- 排除不需要参与打包/编译的目录 -->
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath=""/>
|
||||
<None Include="packaging/_._" Pack="true" PackagePath="lib/net8.0/_._"/>
|
||||
<None Include="packaging/_._" Pack="true" PackagePath="lib/net9.0/_._"/>
|
||||
<None Include="packaging/_._" Pack="true" PackagePath="lib/net10.0/_._"/>
|
||||
<None Remove="GFramework.Core\**"/>
|
||||
<None Remove="GFramework.Game\**"/>
|
||||
<None Remove="GFramework.Godot\**"/>
|
||||
@ -55,6 +60,7 @@
|
||||
<None Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
|
||||
<None Remove="GFramework.SourceGenerators.Attributes\**"/>
|
||||
<None Remove="Godot\**"/>
|
||||
<None Remove="GFramework.Game.Tests\**"/>
|
||||
</ItemGroup>
|
||||
<!-- 聚合核心模块 -->
|
||||
<ItemGroup>
|
||||
@ -93,6 +99,7 @@
|
||||
<Compile Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
|
||||
<Compile Remove="GFramework.SourceGenerators.Attributes\**"/>
|
||||
<Compile Remove="Godot\**"/>
|
||||
<Compile Remove="GFramework.Game.Tests\**"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="GFramework.Core\**"/>
|
||||
@ -117,9 +124,13 @@
|
||||
<EmbeddedResource Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
|
||||
<EmbeddedResource Remove="GFramework.SourceGenerators.Attributes\**"/>
|
||||
<EmbeddedResource Remove="Godot\**"/>
|
||||
<EmbeddedResource Remove="GFramework.Game.Tests\**"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/>
|
||||
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="local-plan\评估\"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Ecs.Arch", "GFra
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Ecs.Arch.Tests", "GFramework.Ecs.Arch.Tests\GFramework.Ecs.Arch.Tests.csproj", "{112CF413-4596-4AA3-B3FE-65532802FDD6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.Tests", "GFramework.Game.Tests\GFramework.Game.Tests.csproj", "{738DC58A-0387-4D75-AA96-1C1D8C29D350}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -234,6 +236,18 @@ Global
|
||||
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x64.Build.0 = Release|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@ -129,7 +129,7 @@ public interface IQuery<TResult>
|
||||
|
||||
### IIocContainer
|
||||
|
||||
IOC 容器接口:
|
||||
IoC 容器接口:
|
||||
|
||||
```csharp
|
||||
public interface IIocContainer
|
||||
|
||||
@ -66,6 +66,23 @@ IoC 容器命名空间。
|
||||
| `IObjectPool<T>` | 对象池接口 |
|
||||
| `ObjectPool<T>` | 对象池实现 |
|
||||
|
||||
### GFramework.Core.Localization
|
||||
|
||||
本地化系统命名空间。
|
||||
|
||||
#### 主要类型
|
||||
|
||||
| 类型 | 说明 |
|
||||
|--------------------------|----------|
|
||||
| `ILocalizationManager` | 本地化管理器接口 |
|
||||
| `ILocalizationTable` | 本地化表接口 |
|
||||
| `ILocalizationString` | 本地化字符串接口 |
|
||||
| `ILocalizationFormatter` | 格式化器接口 |
|
||||
| `LocalizationConfig` | 本地化配置类 |
|
||||
| `LocalizationManager` | 本地化管理器实现 |
|
||||
| `LocalizationTable` | 本地化表实现 |
|
||||
| `LocalizationString` | 本地化字符串实现 |
|
||||
|
||||
## 常用 API
|
||||
|
||||
### Architecture
|
||||
@ -247,6 +264,99 @@ public class BindableProperty<T>
|
||||
}
|
||||
```
|
||||
|
||||
### ILocalizationManager
|
||||
|
||||
```csharp
|
||||
public interface ILocalizationManager : ISystem
|
||||
{
|
||||
// 获取当前语言代码
|
||||
string CurrentLanguage { get; }
|
||||
|
||||
// 获取当前文化信息
|
||||
CultureInfo CurrentCulture { get; }
|
||||
|
||||
// 获取可用语言列表
|
||||
IReadOnlyList<string> AvailableLanguages { get; }
|
||||
|
||||
// 设置当前语言
|
||||
void SetLanguage(string languageCode);
|
||||
|
||||
// 获取本地化表
|
||||
ILocalizationTable GetTable(string tableName);
|
||||
|
||||
// 获取本地化文本
|
||||
string GetText(string table, string key);
|
||||
|
||||
// 获取本地化字符串(支持变量)
|
||||
ILocalizationString GetString(string table, string key);
|
||||
|
||||
// 尝试获取本地化文本
|
||||
bool TryGetText(string table, string key, out string text);
|
||||
|
||||
// 注册格式化器
|
||||
void RegisterFormatter(string name, ILocalizationFormatter formatter);
|
||||
|
||||
// 订阅语言变化事件
|
||||
void SubscribeToLanguageChange(Action<string> callback);
|
||||
|
||||
// 取消订阅语言变化事件
|
||||
void UnsubscribeFromLanguageChange(Action<string> callback);
|
||||
}
|
||||
```
|
||||
|
||||
### ILocalizationString
|
||||
|
||||
```csharp
|
||||
public interface ILocalizationString
|
||||
{
|
||||
// 获取表名
|
||||
string Table { get; }
|
||||
|
||||
// 获取键名
|
||||
string Key { get; }
|
||||
|
||||
// 添加变量
|
||||
ILocalizationString WithVariable(string name, object value);
|
||||
|
||||
// 批量添加变量
|
||||
ILocalizationString WithVariables(params (string name, object value)[] variables);
|
||||
|
||||
// 格式化并返回文本
|
||||
string Format();
|
||||
|
||||
// 获取原始文本
|
||||
string GetRaw();
|
||||
|
||||
// 检查键是否存在
|
||||
bool Exists();
|
||||
}
|
||||
```
|
||||
|
||||
### LocalizationConfig
|
||||
|
||||
```csharp
|
||||
public class LocalizationConfig
|
||||
{
|
||||
// 默认语言代码
|
||||
public string DefaultLanguage { get; set; } = "eng";
|
||||
|
||||
// 回退语言代码
|
||||
public string FallbackLanguage { get; set; } = "eng";
|
||||
|
||||
// 本地化文件路径
|
||||
public string LocalizationPath { get; set; } = "res://localization";
|
||||
|
||||
// 用户覆盖路径
|
||||
public string OverridePath { get; set; } = "user://localization_override";
|
||||
|
||||
// 是否启用热重载
|
||||
public bool EnableHotReload { get; set; } = true;
|
||||
|
||||
// 是否在加载时验证
|
||||
public bool ValidateOnLoad { get; set; } = true;
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展方法
|
||||
|
||||
### 架构扩展
|
||||
@ -406,6 +516,36 @@ public class PlayerSystem : AbstractSystem
|
||||
}
|
||||
```
|
||||
|
||||
### 使用本地化
|
||||
|
||||
```csharp
|
||||
// 初始化本地化管理器
|
||||
var config = new LocalizationConfig
|
||||
{
|
||||
DefaultLanguage = "eng",
|
||||
LocalizationPath = "res://localization"
|
||||
};
|
||||
var locManager = new LocalizationManager(config);
|
||||
locManager.Initialize();
|
||||
|
||||
// 获取简单文本
|
||||
string title = locManager.GetText("common", "game.title");
|
||||
|
||||
// 使用变量
|
||||
var message = locManager.GetString("common", "ui.message.welcome")
|
||||
.WithVariable("playerName", "Alice")
|
||||
.Format();
|
||||
|
||||
// 切换语言
|
||||
locManager.SetLanguage("zhs");
|
||||
|
||||
// 监听语言变化
|
||||
locManager.SubscribeToLanguageChange(language =>
|
||||
{
|
||||
Console.WriteLine($"Language changed to: {language}");
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
更多详情请查看各模块的详细文档。
|
||||
|
||||
@ -118,6 +118,10 @@ GFramework 是一个开源的游戏开发框架,我们欢迎所有形式的贡
|
||||
|
||||
## 开发环境设置
|
||||
|
||||
当前推荐的项目相关环境、CLI 与 AI 可用工具清单请查看:
|
||||
|
||||
- [开发环境能力清单](./contributor/development-environment.md)
|
||||
|
||||
### 前置要求
|
||||
|
||||
- **.NET SDK**:8.0、9.0 或 10.0
|
||||
|
||||
96
docs/zh-CN/contributor/development-environment.md
Normal file
96
docs/zh-CN/contributor/development-environment.md
Normal file
@ -0,0 +1,96 @@
|
||||
# 开发环境能力清单
|
||||
|
||||
这份文档只记录对 `GFramework` 当前开发和 AI 协作真正有用的环境能力,不收录与本项目无关的系统工具。
|
||||
|
||||
如果某个工具没有出现在这里,默认表示它对当前仓库不是必需项,AI 也不应因为“系统里刚好装了”就优先使用它。
|
||||
|
||||
## 当前环境基线
|
||||
|
||||
当前仓库验证基线是:
|
||||
|
||||
- **运行环境**:WSL2
|
||||
- **发行版**:Ubuntu 24.04 LTS
|
||||
- **Shell**:`bash`
|
||||
|
||||
机器可读的环境数据分成两层:
|
||||
|
||||
- `GFramework/.ai/environment/tools.raw.yaml`:完整事实采集
|
||||
- `GFramework/.ai/environment/tools.ai.yaml`:给 AI 看的精简决策提示
|
||||
|
||||
AI 应优先读取 `tools.ai.yaml`,只有在需要追溯完整事实时才查看 `tools.raw.yaml`。
|
||||
|
||||
## 当前项目需要的运行时
|
||||
|
||||
| 工具 | 是否需要 | 在 GFramework 中的用途 |
|
||||
|-----------|------|---------------------------------|
|
||||
| `dotnet` | 必需 | 构建、测试、打包整个解决方案 |
|
||||
| `python3` | 推荐 | 运行本地辅助脚本、环境采集和轻量自动化 |
|
||||
| `node` | 推荐 | 作为文档工具链的 JavaScript 运行时 |
|
||||
| `bun` | 推荐 | 安装并预览 `docs/` 下的 VitePress 文档站点 |
|
||||
|
||||
## 当前项目需要的命令行工具
|
||||
|
||||
| 工具 | 是否需要 | 在 GFramework 中的用途 |
|
||||
|----------|------|-----------------------------------------------|
|
||||
| `git` | 必需 | 提交代码、查看 diff、审查变更 |
|
||||
| `bash` | 必需 | 执行仓库脚本,例如 `scripts/validate-csharp-naming.sh` |
|
||||
| `rg` | 必需 | 在仓库中快速搜索代码和文档 |
|
||||
| `jq` | 推荐 | 处理 JSON 输出,便于本地脚本和 AI 做结构化检查 |
|
||||
| `docker` | 可选 | 运行 MegaLinter 等容器化检查工具 |
|
||||
|
||||
这里只保留和当前仓库直接相关的 CLI。像 `kubectl`、`terraform`、`helm`、`java`、数据库客户端等工具,即使系统已安装,也不进入正式清单。
|
||||
|
||||
## Python 包
|
||||
|
||||
Python 包只记录两类内容:
|
||||
|
||||
- 当前环境里已经存在、对开发辅助有价值的包
|
||||
- 明确对 AI/脚本化开发有帮助、后续可能会安装的包
|
||||
|
||||
| 包 | 当前状态 | 用途 |
|
||||
|------------|---------|---------------------|
|
||||
| `requests` | 当前环境已安装 | 用于简单 HTTP 调用和脚本集成 |
|
||||
| `rich` | 当前环境已安装 | 用于更易读的终端输出 |
|
||||
| `openai` | 当前环境可选 | 用于脚本化调用 OpenAI API |
|
||||
| `tiktoken` | 当前环境可选 | 用于 token 估算和上下文检查 |
|
||||
| `pydantic` | 当前环境可选 | 用于结构化配置和模式校验 |
|
||||
| `pytest` | 当前环境可选 | 用于 Python 辅助脚本的小型测试 |
|
||||
|
||||
如果某个 Python 包与当前仓库没有直接关系,就不要加入清单。
|
||||
|
||||
## AI 使用约定
|
||||
|
||||
AI 在这个仓库里应优先使用:
|
||||
|
||||
- `rg` 做文本搜索
|
||||
- `jq` 做 JSON 检查
|
||||
- `bash` 执行仓库脚本
|
||||
- `dotnet` 做构建和测试
|
||||
- `bun` 做文档预览
|
||||
- `python3 + requests` 做轻量本地辅助脚本
|
||||
|
||||
AI 不应直接把原始探测数据当成决策规则;应以 `tools.ai.yaml` 中的推荐和 fallback 为准。如果确实需要引入新工具,应先更新环境清单,再在任务中使用。
|
||||
|
||||
## 如何刷新环境清单
|
||||
|
||||
使用仓库脚本先采集原始环境,再生成 AI 版本:
|
||||
|
||||
```bash
|
||||
# 输出原始环境清单到终端
|
||||
bash scripts/collect-dev-environment.sh --check
|
||||
|
||||
# 写回原始清单
|
||||
bash scripts/collect-dev-environment.sh --write
|
||||
|
||||
# 由原始清单生成 AI 决策清单
|
||||
python3 scripts/generate-ai-environment.py
|
||||
```
|
||||
|
||||
## 维护规则
|
||||
|
||||
- 目标不是记录“这台机器装了什么”,而是记录“GFramework 开发和 AI 协作实际该用什么”。
|
||||
- 新工具只有在满足以下条件之一时才应加入清单:
|
||||
- 当前仓库构建、测试、文档或验证直接依赖它
|
||||
- AI 在当前仓库中会高频使用,且能明显提升效率
|
||||
- 新贡献者配置当前仓库开发环境时确实需要知道它
|
||||
- 不满足上述条件的工具,不写入文档,也不写入 `.ai/environment/tools.raw.yaml` / `.ai/environment/tools.ai.yaml`。
|
||||
@ -1,608 +1,230 @@
|
||||
# Architecture 包使用说明
|
||||
# Architecture 架构详解
|
||||
|
||||
> 深入了解 GFramework 的核心架构设计和实现
|
||||
|
||||
## 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [架构设计](#架构设计)
|
||||
- [生命周期管理](#生命周期管理)
|
||||
- [组件注册](#组件注册)
|
||||
- [模块系统](#模块系统)
|
||||
- [最佳实践](#最佳实践)
|
||||
- [API 参考](#api-参考)
|
||||
|
||||
## 概述
|
||||
|
||||
Architecture 包是整个框架的核心,提供了基于分层架构模式的应用程序架构基础。它实现了依赖注入(IoC)
|
||||
容器、组件生命周期管理,以及命令、查询、事件的统一调度机制。
|
||||
Architecture 是 GFramework 的核心类,负责管理整个应用的生命周期、组件注册和模块管理。从 v1.1.0 开始,Architecture
|
||||
采用模块化设计,将职责分离到专门的管理器中。
|
||||
|
||||
**注意**:本框架的 Core 模块与 Godot 解耦,Godot 相关集成在 GFramework.Godot 包中实现。
|
||||
### 设计目标
|
||||
|
||||
## 核心接口
|
||||
- **单一职责**: 每个管理器只负责一个明确的功能
|
||||
- **类型安全**: 基于泛型的组件获取和注册
|
||||
- **生命周期管理**: 自动的初始化和销毁机制
|
||||
- **可扩展性**: 支持模块和钩子扩展
|
||||
- **向后兼容**: 保持公共 API 稳定
|
||||
|
||||
### IArchitecture
|
||||
### 核心组件
|
||||
|
||||
架构接口,定义了框架的核心功能契约。
|
||||
|
||||
**主要职责:**
|
||||
|
||||
- **组件注册**:注册 System、Model、Utility
|
||||
- **组件获取**:从容器中获取已注册的组件
|
||||
- **命令处理**:发送并执行命令
|
||||
- **查询处理**:发送并执行查询
|
||||
- **事件管理**:发送、注册、注销事件
|
||||
- **模块管理**:安装和管理架构模块
|
||||
- **生命周期管理**:管理架构的初始化、运行和销毁阶段
|
||||
|
||||
**核心方法:**
|
||||
```csharp
|
||||
// 组件注册
|
||||
void RegisterSystem<TSystem>(TSystem system) where TSystem : ISystem;
|
||||
void RegisterModel<TModel>(TModel model) where TModel : IModel;
|
||||
void RegisterUtility<TUtility>(TUtility utility) where TUtility : IUtility;
|
||||
|
||||
// 组件获取(通过容器)
|
||||
T GetModel<T>() where T : class, IModel;
|
||||
T GetSystem<T>() where T : class, ISystem;
|
||||
T GetUtility<T>() where T : class, IUtility;
|
||||
|
||||
// 命令处理
|
||||
void SendCommand(ICommand command);
|
||||
TResult SendCommand<TResult>(ICommand<TResult> command);
|
||||
|
||||
// 查询处理
|
||||
TResult SendQuery<TResult>(IQuery<TResult> query);
|
||||
|
||||
// 事件管理
|
||||
void SendEvent<T>() where T : new();
|
||||
void SendEvent<T>(T e);
|
||||
IUnRegister RegisterEvent<T>(Action<T> onEvent);
|
||||
void UnRegisterEvent<T>(Action<T> onEvent);
|
||||
```
|
||||
Architecture (核心协调器)
|
||||
├── ArchitectureLifecycle (生命周期管理)
|
||||
├── ArchitectureComponentRegistry (组件注册)
|
||||
└── ArchitectureModules (模块管理)
|
||||
```
|
||||
|
||||
### IArchitecturePhaseAware
|
||||
## 架构设计
|
||||
|
||||
架构阶段感知接口,允许组件监听架构阶段变化。
|
||||
### 设计模式
|
||||
|
||||
**核心方法:**
|
||||
```csharp
|
||||
void OnArchitecturePhase(ArchitecturePhase phase);
|
||||
Architecture 采用以下设计模式:
|
||||
|
||||
1. **组合模式 (Composition)**: Architecture 组合三个管理器
|
||||
2. **委托模式 (Delegation)**: 方法调用委托给专门的管理器
|
||||
3. **协调器模式 (Coordinator)**: Architecture 作为协调器统一对外接口
|
||||
|
||||
### 类图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Architecture │
|
||||
│ - _lifecycle: ArchitectureLifecycle │
|
||||
│ - _componentRegistry: ArchitectureComponentRegistry│
|
||||
│ - _modules: ArchitectureModules │
|
||||
│ - _logger: ILogger │
|
||||
│ │
|
||||
│ + RegisterSystem<T>() │
|
||||
│ + RegisterModel<T>() │
|
||||
│ + RegisterUtility<T>() │
|
||||
│ + InstallModule() │
|
||||
│ + InitializeAsync() │
|
||||
│ + DestroyAsync() │
|
||||
│ + event PhaseChanged │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│ │ │
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||||
│ Lifecycle │ │ ComponentRegistry│ │ Modules │
|
||||
│ │ │ │ │ │
|
||||
│ - 阶段管理 │ │ - System 注册 │ │ - 模块安装 │
|
||||
│ - 钩子管理 │ │ - Model 注册 │ │ - 行为注册 │
|
||||
│ - 初始化 │ │ - Utility 注册 │ │ │
|
||||
│ - 销毁 │ │ - 生命周期注册 │ │ │
|
||||
└──────────────┘ └──────────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### IArchitectureModule
|
||||
### 构造函数初始化
|
||||
|
||||
架构模块接口,支持模块化架构扩展。
|
||||
从 v1.1.0 开始,所有管理器在构造函数中初始化:
|
||||
|
||||
**核心方法:**
|
||||
```csharp
|
||||
void Install(IArchitecture architecture);
|
||||
```
|
||||
|
||||
### IAsyncInitializable
|
||||
|
||||
异步初始化接口,支持组件异步初始化。
|
||||
|
||||
**核心方法:**
|
||||
```csharp
|
||||
Task InitializeAsync();
|
||||
```
|
||||
|
||||
## 核心类
|
||||
|
||||
### Architecture 架构基类
|
||||
|
||||
架构基类,实现了 `IArchitecture` 接口,提供完整的架构功能实现。
|
||||
|
||||
**构造函数参数:**
|
||||
```csharp
|
||||
public abstract class Architecture(
|
||||
protected Architecture(
|
||||
IArchitectureConfiguration? configuration = null,
|
||||
IEnvironment? environment = null,
|
||||
IArchitectureServices? services = null,
|
||||
IArchitectureContext? context = null
|
||||
)
|
||||
```
|
||||
|
||||
**特性:**
|
||||
|
||||
- **阶段式生命周期管理**
|
||||
:支持多个架构阶段(BeforeUtilityInit、AfterUtilityInit、BeforeModelInit、AfterModelInit、BeforeSystemInit、AfterSystemInit、Ready、FailedInitialization、Destroying、Destroyed)
|
||||
- **模块安装系统**:支持通过 `InstallModule` 扩展架构功能
|
||||
- **异步初始化**:支持同步和异步两种初始化方式
|
||||
- **IoC 容器集成**:内置依赖注入容器
|
||||
- **事件系统集成**:集成类型化事件系统
|
||||
- **与平台无关**:Core 模块不依赖 Godot,可以在任何 .NET 环境中使用
|
||||
- **严格的阶段验证**:可配置的阶段转换验证机制
|
||||
- **组件生命周期管理**:自动管理组件的初始化和销毁
|
||||
|
||||
**架构阶段:**
|
||||
```csharp
|
||||
public enum ArchitecturePhase
|
||||
IArchitectureContext? context = null)
|
||||
{
|
||||
None = 0, // 初始阶段
|
||||
BeforeUtilityInit = 1, // 工具初始化前
|
||||
AfterUtilityInit = 2, // 工具初始化后
|
||||
BeforeModelInit = 3, // 模型初始化前
|
||||
AfterModelInit = 4, // 模型初始化后
|
||||
BeforeSystemInit = 5, // 系统初始化前
|
||||
AfterSystemInit = 6, // 系统初始化后
|
||||
Ready = 7, // 就绪状态
|
||||
FailedInitialization = 8, // 初始化失败
|
||||
Destroying = 9, // 正在销毁
|
||||
Destroyed = 10 // 已销毁
|
||||
Configuration = configuration ?? new ArchitectureConfiguration();
|
||||
Environment = environment ?? new DefaultEnvironment();
|
||||
Services = services ?? new ArchitectureServices();
|
||||
_context = context;
|
||||
|
||||
// 初始化 Logger
|
||||
LoggerFactoryResolver.Provider = Configuration.LoggerProperties.LoggerFactoryProvider;
|
||||
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
|
||||
|
||||
// 初始化管理器
|
||||
_lifecycle = new ArchitectureLifecycle(this, Configuration, Services, _logger);
|
||||
_componentRegistry = new ArchitectureComponentRegistry(this, Configuration, Services, _lifecycle, _logger);
|
||||
_modules = new ArchitectureModules(this, Services, _logger);
|
||||
}
|
||||
```
|
||||
|
||||
**初始化流程:**
|
||||
**优势**:
|
||||
|
||||
1. 创建架构实例(传入配置或使用默认配置)
|
||||
2. 初始化基础上下文和日志系统
|
||||
3. 调用用户自定义的 `Init()` 方法
|
||||
4. 按顺序初始化组件:
|
||||
- 工具初始化(BeforeUtilityInit → AfterUtilityInit)
|
||||
- 模型初始化(BeforeModelInit → AfterModelInit)
|
||||
- 系统初始化(BeforeSystemInit → AfterSystemInit)
|
||||
5. 冻结 IOC 容器
|
||||
6. 进入 Ready 阶段
|
||||
- 消除 `null!` 断言,提高代码安全性
|
||||
- 对象在构造后立即可用
|
||||
- 符合"构造即完整"原则
|
||||
- 可以在 InitializeAsync 之前访问事件
|
||||
|
||||
**销毁流程:**
|
||||
## 生命周期管理
|
||||
|
||||
1. 进入 Destroying 阶段
|
||||
2. 按注册逆序销毁所有实现了 `IDisposable` 的组件
|
||||
3. 进入 Destroyed 阶段
|
||||
4. 清理所有资源
|
||||
### 架构阶段
|
||||
|
||||
**使用示例:**
|
||||
Architecture 定义了 11 个生命周期阶段:
|
||||
|
||||
| 阶段 | 说明 | 触发时机 |
|
||||
|------------------------|--------------|------------------|
|
||||
| `None` | 初始状态 | 构造函数完成后 |
|
||||
| `BeforeUtilityInit` | Utility 初始化前 | 开始初始化 Utility |
|
||||
| `AfterUtilityInit` | Utility 初始化后 | 所有 Utility 初始化完成 |
|
||||
| `BeforeModelInit` | Model 初始化前 | 开始初始化 Model |
|
||||
| `AfterModelInit` | Model 初始化后 | 所有 Model 初始化完成 |
|
||||
| `BeforeSystemInit` | System 初始化前 | 开始初始化 System |
|
||||
| `AfterSystemInit` | System 初始化后 | 所有 System 初始化完成 |
|
||||
| `Ready` | 就绪状态 | 所有组件初始化完成 |
|
||||
| `Destroying` | 销毁中 | 开始销毁 |
|
||||
| `Destroyed` | 已销毁 | 销毁完成 |
|
||||
| `FailedInitialization` | 初始化失败 | 初始化过程中发生异常 |
|
||||
|
||||
### 阶段转换
|
||||
|
||||
```
|
||||
正常流程:
|
||||
None → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit
|
||||
→ BeforeSystemInit → AfterSystemInit → Ready → Destroying → Destroyed
|
||||
|
||||
异常流程:
|
||||
Any → FailedInitialization
|
||||
```
|
||||
|
||||
### 阶段事件
|
||||
|
||||
可以通过 `PhaseChanged` 事件监听阶段变化:
|
||||
|
||||
```csharp
|
||||
// 1. 定义你的架构(继承 Architecture 基类)
|
||||
public class GameArchitecture : Architecture
|
||||
public class MyArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
// 注册 Model
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterModel(new InventoryModel());
|
||||
|
||||
// 注册 System
|
||||
RegisterSystem(new GameplaySystem());
|
||||
RegisterSystem(new SaveSystem());
|
||||
|
||||
// 注册 Utility
|
||||
RegisterUtility(new StorageUtility());
|
||||
RegisterUtility(new TimeUtility());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 创建并初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize();
|
||||
|
||||
// 或者异步初始化
|
||||
// var architecture = new GameArchitecture();
|
||||
// await architecture.InitializeAsync();
|
||||
|
||||
// 3. 等待架构就绪
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 4. 通过依赖注入使用架构
|
||||
// 在 Controller 或其他组件中获取架构实例
|
||||
using GFramework.Core.Abstractions.Controller;
|
||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
[ContextAware]
|
||||
public partial class GameController : IController
|
||||
{
|
||||
public void Start()
|
||||
{
|
||||
// 获取 Model(使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口))
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
|
||||
// 发送命令
|
||||
this.SendCommand(new StartGameCommand());
|
||||
|
||||
// 发送查询
|
||||
var score = this.SendQuery(new GetScoreQuery());
|
||||
|
||||
// 注册事件
|
||||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
|
||||
}
|
||||
|
||||
private void OnPlayerDied(PlayerDiedEvent e)
|
||||
{
|
||||
// 处理玩家死亡事件
|
||||
// 监听阶段变化
|
||||
PhaseChanged += phase =>
|
||||
{
|
||||
Console.WriteLine($"Phase changed to: {phase}");
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**核心方法与属性:**
|
||||
### 生命周期钩子
|
||||
|
||||
#### 初始化方法
|
||||
|
||||
**Initialize()**
|
||||
|
||||
同步初始化方法,阻塞当前线程直到初始化完成。
|
||||
```csharp
|
||||
public void Initialize()
|
||||
```
|
||||
|
||||
**特点:**
|
||||
|
||||
- 阻塞式初始化
|
||||
- 适用于简单场景或控制台应用
|
||||
- 初始化失败时抛出异常并进入 `FailedInitialization` 阶段
|
||||
|
||||
使用示例:
|
||||
```csharp
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize(); // 阻塞等待初始化完成
|
||||
```
|
||||
|
||||
异常处理:如果初始化过程中发生异常,架构会进入 `FailedInitialization` 阶段。
|
||||
|
||||
**InitializeAsync()**
|
||||
|
||||
异步初始化方法,返回 Task 以便调用者可以等待初始化完成。
|
||||
```csharp
|
||||
public async Task InitializeAsync()
|
||||
```
|
||||
|
||||
**特点:**
|
||||
|
||||
- 非阻塞式初始化
|
||||
- 支持异步组件初始化
|
||||
- 适用于需要异步加载资源的场景
|
||||
- 初始化失败时抛出异常并进入 `FailedInitialization` 阶段
|
||||
|
||||
使用示例:
|
||||
```csharp
|
||||
var architecture = new GameArchitecture();
|
||||
await architecture.InitializeAsync(); // 异步等待初始化完成
|
||||
```
|
||||
|
||||
优势:
|
||||
- 支持异步初始化 Model 和 System
|
||||
- 可以利用异步 I/O 操作(如异步加载数据)
|
||||
- 提高初始化性能
|
||||
- 不会阻塞主线程
|
||||
|
||||
#### 模块管理
|
||||
|
||||
**InstallModule(IArchitectureModule module)**
|
||||
|
||||
安装架构模块,用于扩展架构功能。
|
||||
```csharp
|
||||
public IArchitectureModule InstallModule(IArchitectureModule module)
|
||||
```
|
||||
|
||||
**返回值:** 返回安装的模块实例
|
||||
|
||||
**特点:**
|
||||
|
||||
- 模块安装时会自动注册生命周期钩子
|
||||
- 模块可以注册额外的组件到架构中
|
||||
- 只能在架构进入 Ready 阶段之前安装模块
|
||||
|
||||
参数:
|
||||
|
||||
- `module`:要安装的模块实例
|
||||
|
||||
使用示例:
|
||||
实现 `IArchitectureLifecycleHook` 接口可以在阶段变化时执行自定义逻辑:
|
||||
|
||||
```csharp
|
||||
// 定义模块
|
||||
public class NetworkModule : IArchitectureModule
|
||||
{
|
||||
public void Install(IArchitecture architecture)
|
||||
{
|
||||
// 注册网络相关的 System 和 Utility
|
||||
architecture.RegisterSystem(new NetworkSystem());
|
||||
architecture.RegisterUtility(new HttpClientUtility());
|
||||
}
|
||||
}
|
||||
|
||||
// 安装模块
|
||||
var architecture = new GameArchitecture();
|
||||
var installedModule = architecture.InstallModule(new NetworkModule());
|
||||
```
|
||||
|
||||
#### 生命周期钩子
|
||||
|
||||
**RegisterLifecycleHook(IArchitectureLifecycle hook)**
|
||||
|
||||
注册生命周期钩子,用于在架构阶段变化时执行自定义逻辑。
|
||||
```csharp
|
||||
public IArchitectureLifecycle RegisterLifecycleHook(IArchitectureLifecycle hook)
|
||||
```
|
||||
|
||||
**返回值:** 返回注册的生命周期钩子实例
|
||||
|
||||
**特点:**
|
||||
|
||||
- 生命周期钩子可以监听所有架构阶段变化
|
||||
- 只能在架构进入 Ready 阶段之前注册
|
||||
- 架构会按注册顺序通知所有钩子
|
||||
|
||||
参数:
|
||||
|
||||
- `hook`:生命周期钩子实例
|
||||
|
||||
使用示例:
|
||||
|
||||
```csharp
|
||||
// 定义生命周期钩子
|
||||
public class PerformanceMonitorHook : IArchitectureLifecycle
|
||||
public class MyLifecycleHook : IArchitectureLifecycleHook
|
||||
{
|
||||
public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case ArchitecturePhase.BeforeModelInit:
|
||||
Console.WriteLine("开始监控 Model 初始化性能");
|
||||
break;
|
||||
case ArchitecturePhase.AfterModelInit:
|
||||
Console.WriteLine("Model 初始化完成,停止监控");
|
||||
break;
|
||||
case ArchitecturePhase.Ready:
|
||||
Console.WriteLine("架构就绪,开始性能统计");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 注册生命周期钩子
|
||||
var architecture = new GameArchitecture();
|
||||
var registeredHook = architecture.RegisterLifecycleHook(new PerformanceMonitorHook());
|
||||
architecture.Initialize();
|
||||
```
|
||||
|
||||
**注意:** 生命周期钩子只能在架构进入 Ready 阶段之前注册。
|
||||
|
||||
#### 属性
|
||||
|
||||
**CurrentPhase**
|
||||
|
||||
获取当前架构的阶段。
|
||||
```csharp
|
||||
public ArchitecturePhase CurrentPhase { get; private set; }
|
||||
```
|
||||
|
||||
**特点:**
|
||||
|
||||
- 只读属性,外部无法直接修改
|
||||
- 实时反映架构的当前状态
|
||||
- 可用于条件判断和状态检查
|
||||
|
||||
使用示例:
|
||||
|
||||
```csharp
|
||||
var architecture = new GameArchitecture();
|
||||
|
||||
// 检查架构是否已就绪
|
||||
if (architecture.CurrentPhase == ArchitecturePhase.Ready)
|
||||
{
|
||||
Console.WriteLine("架构已就绪,可以开始游戏");
|
||||
}
|
||||
|
||||
// 在异步操作中检查阶段变化
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
while (architecture.CurrentPhase != ArchitecturePhase.Ready)
|
||||
{
|
||||
Console.WriteLine($"当前阶段: {architecture.CurrentPhase}");
|
||||
await Task.Delay(100);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Context**
|
||||
|
||||
获取架构上下文,提供对架构服务的访问。
|
||||
```csharp
|
||||
public IArchitectureContext Context { get; }
|
||||
```
|
||||
|
||||
**特点:**
|
||||
|
||||
- 提供对架构核心服务的访问
|
||||
- 包含事件总线、命令总线、查询总线等
|
||||
- 是架构内部服务的统一入口
|
||||
|
||||
使用示例:
|
||||
|
||||
```csharp
|
||||
// 通过 Context 访问服务
|
||||
var context = architecture.Context;
|
||||
var eventBus = context.EventBus;
|
||||
var commandBus = context.CommandBus;
|
||||
var queryBus = context.QueryBus;
|
||||
var environment = context.Environment;
|
||||
```
|
||||
|
||||
#### 高级特性
|
||||
|
||||
```csharp
|
||||
// 1. 使用自定义配置
|
||||
var config = new ArchitectureConfiguration
|
||||
{
|
||||
ArchitectureProperties = new ArchitectureProperties
|
||||
{
|
||||
StrictPhaseValidation = true, // 启用严格阶段验证
|
||||
AllowLateRegistration = false // 禁止就绪后注册组件
|
||||
}
|
||||
};
|
||||
var architecture = new GameArchitecture(configuration: config);
|
||||
|
||||
// 2. 模块安装
|
||||
var module = new GameModule();
|
||||
architecture.InstallModule(module);
|
||||
|
||||
// 3. 监听架构阶段变化
|
||||
public class GamePhaseListener : IArchitecturePhaseAware
|
||||
{
|
||||
public void OnArchitecturePhase(ArchitecturePhase phase)
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case ArchitecturePhase.Ready:
|
||||
Console.WriteLine("架构已就绪,可以开始游戏了");
|
||||
Console.WriteLine("Architecture is ready!");
|
||||
break;
|
||||
case ArchitecturePhase.Destroying:
|
||||
Console.WriteLine("架构正在销毁");
|
||||
Console.WriteLine("Architecture is being destroyed!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 生命周期钩子
|
||||
public class LifecycleHook : IArchitectureLifecycle
|
||||
{
|
||||
public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||
{
|
||||
Console.WriteLine($"架构阶段变化: {phase}");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 等待架构就绪
|
||||
public async Task WaitForArchitectureReady()
|
||||
{
|
||||
var architecture = new GameArchitecture();
|
||||
var initTask = architecture.InitializeAsync();
|
||||
|
||||
// 可以在其他地方等待架构就绪
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
Console.WriteLine("架构已就绪");
|
||||
}
|
||||
// 注册钩子
|
||||
architecture.RegisterLifecycleHook(new MyLifecycleHook());
|
||||
```
|
||||
|
||||
### ArchitectureConfiguration 架构配置类
|
||||
### 初始化流程
|
||||
|
||||
架构配置类,用于配置架构的行为。
|
||||
```
|
||||
1. 创建 Architecture 实例
|
||||
└─> 构造函数初始化管理器
|
||||
|
||||
**主要配置项:**
|
||||
2. 调用 InitializeAsync() 或 Initialize()
|
||||
├─> 初始化环境 (Environment.Initialize())
|
||||
├─> 注册内置服务模块
|
||||
├─> 初始化架构上下文
|
||||
├─> 执行服务钩子
|
||||
├─> 初始化服务模块
|
||||
├─> 调用 OnInitialize() (用户注册组件)
|
||||
├─> 初始化所有组件
|
||||
│ ├─> BeforeUtilityInit → 初始化 Utility → AfterUtilityInit
|
||||
│ ├─> BeforeModelInit → 初始化 Model → AfterModelInit
|
||||
│ └─> BeforeSystemInit → 初始化 System → AfterSystemInit
|
||||
├─> 冻结 IoC 容器
|
||||
└─> 进入 Ready 阶段
|
||||
|
||||
```csharp
|
||||
public class ArchitectureConfiguration : IArchitectureConfiguration
|
||||
{
|
||||
public IArchitectureProperties ArchitectureProperties { get; set; } = new ArchitectureProperties();
|
||||
public IEnvironmentProperties EnvironmentProperties { get; set; } = new EnvironmentProperties();
|
||||
public ILoggerProperties LoggerProperties { get; set; } = new LoggerProperties();
|
||||
}
|
||||
|
||||
public class ArchitectureProperties
|
||||
{
|
||||
public bool StrictPhaseValidation { get; set; } = false; // 严格阶段验证
|
||||
public bool AllowLateRegistration { get; set; } = true; // 允许延迟注册
|
||||
}
|
||||
3. 等待就绪 (可选)
|
||||
└─> await architecture.WaitUntilReadyAsync()
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
### 销毁流程
|
||||
|
||||
```csharp
|
||||
var config = new ArchitectureConfiguration
|
||||
{
|
||||
ArchitectureProperties = new ArchitectureProperties
|
||||
{
|
||||
StrictPhaseValidation = true, // 启用严格阶段验证
|
||||
AllowLateRegistration = false // 禁止就绪后注册组件
|
||||
},
|
||||
LoggerProperties = new LoggerProperties
|
||||
{
|
||||
LoggerFactoryProvider = new ConsoleLoggerFactoryProvider() // 自定义日志工厂
|
||||
}
|
||||
};
|
||||
|
||||
var architecture = new GameArchitecture(configuration: config);
|
||||
```
|
||||
|
||||
### ArchitectureServices 架构服务类
|
||||
|
||||
架构服务类,管理命令总线、查询总线、IOC容器和类型事件系统。
|
||||
|
||||
**核心服务:**
|
||||
|
||||
- `IIocContainer Container`:依赖注入容器
|
||||
- `IEventBus EventBus`:事件总线
|
||||
- `ICommandBus CommandBus`:命令总线
|
||||
- `IQueryBus QueryBus`:查询总线
|
||||
|
||||
### ArchitectureContext 架构上下文类
|
||||
|
||||
架构上下文类,提供对架构服务的访问。
|
||||
|
||||
**功能:**
|
||||
|
||||
- 统一访问架构核心服务
|
||||
- 管理服务实例的生命周期
|
||||
- 提供服务解析功能
|
||||
|
||||
### GameContext 游戏上下文类
|
||||
|
||||
游戏上下文类,管理架构上下文与类型的绑定关系。
|
||||
|
||||
**功能:**
|
||||
|
||||
- 维护架构类型与上下文实例的映射
|
||||
- 提供全局上下文访问
|
||||
- 支持多架构实例管理
|
||||
|
||||
## 设计模式
|
||||
|
||||
### 1. 依赖注入
|
||||
|
||||
通过构造函数注入或容器解析获取架构实例。
|
||||
|
||||
### 2. 控制反转 (IoC)
|
||||
|
||||
使用内置 IoC 容器管理组件生命周期和依赖关系。
|
||||
|
||||
### 3. 命令模式
|
||||
|
||||
通过 `ICommand` 封装所有用户操作。
|
||||
|
||||
### 4. 查询模式 (CQRS)
|
||||
|
||||
通过 `IQuery<T>` 分离查询和命令操作。
|
||||
|
||||
### 5. 观察者模式
|
||||
|
||||
通过事件系统实现组件间的松耦合通信。
|
||||
|
||||
### 6. 阶段式生命周期管理
|
||||
|
||||
通过 `ArchitecturePhase` 枚举和生命周期钩子管理架构状态。
|
||||
|
||||
### 7. 组合优于继承
|
||||
|
||||
通过接口组合获得不同能力,而不是深层继承链。
|
||||
|
||||
### 8. 模块化设计
|
||||
|
||||
通过 `IArchitectureModule` 实现架构的可扩展性。
|
||||
|
||||
### 9. 工厂模式
|
||||
|
||||
通过配置对象和工厂方法创建架构实例。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **保持架构类简洁**:只在 `Init()` 中注册组件,不要包含业务逻辑
|
||||
2. **合理划分职责**:
|
||||
- Model:数据和状态
|
||||
- System:业务逻辑和规则
|
||||
- Utility:无状态的工具方法
|
||||
3. **使用依赖注入**:通过构造函数注入架构实例,便于测试
|
||||
4. **事件命名规范**:使用过去式命名事件类,如 `PlayerDiedEvent`
|
||||
5. **避免循环依赖**:System 不应直接引用 System,应通过事件通信
|
||||
6. **使用模块扩展**:通过 `IArchitectureModule` 实现架构的可扩展性
|
||||
7. **Core 模块与平台解耦**:GFramework.Core 不包含 Godot 相关代码,Godot 集成在单独模块中
|
||||
8. **合理配置阶段验证**:根据项目需求配置 `StrictPhaseValidation` 和 `AllowLateRegistration`
|
||||
9. **及时处理初始化异常**:捕获并处理架构初始化过程中的异常
|
||||
10. **使用异步初始化**:对于需要加载大量资源的场景,优先使用 `InitializeAsync()`
|
||||
|
||||
## 相关包
|
||||
|
||||
- **command** - 命令模式实现
|
||||
- **query** - 查询模式实现
|
||||
- **events** - 事件系统
|
||||
- **ioc** - IoC 容器
|
||||
- **model** - 数据模型
|
||||
- **system** - 业务系统
|
||||
- **utility** - 工具类
|
||||
- **GFramework.Godot** - Godot 特定集成(GodotNode 扩展、GodotLogger 等)
|
||||
- **extensions** - 扩展方法
|
||||
- **logging** - 日志系统
|
||||
- **environment** - 环境管理
|
||||
1. 调用 DestroyAsync() 或 Destroy()
|
||||
├─> 检查当前阶段 (如果是 None 或已销毁则直接返回)
|
||||
├─> 进入 Destroying 阶段
|
||||
├─> 逆序销毁所有组件
|
||||
│ ├─> 优先调用 IAsyncDestroyable.DestroyAsync()
|
||||
│ └─> 否则调用 IDestroyable.Destroy()
|
||||
├─> 销毁服务模块
|
||||
├─> 清空 IoC 容器
|
||||
└─> 进入 Destroyed 阶段
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**许可证**:Apache 2.0
|
||||
**版本**: 1.1.0
|
||||
**更新日期**: 2026-03-17
|
||||
**相关文档**:
|
||||
|
||||
- [核心框架概述](./index.md)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
501
docs/zh-CN/core/localization.md
Normal file
501
docs/zh-CN/core/localization.md
Normal file
@ -0,0 +1,501 @@
|
||||
# Localization 本地化系统
|
||||
|
||||
## 概述
|
||||
|
||||
Localization 包提供了完整的多语言本地化支持,实现了游戏文本的国际化管理。通过本地化系统,可以轻松实现多语言切换、动态变量替换、回退机制等功能。
|
||||
|
||||
本地化系统是 GFramework 架构中的 System 层组件,与其他系统无缝集成,支持类型安全的 API 和流畅的使用体验。
|
||||
|
||||
## 核心接口
|
||||
|
||||
### ILocalizationManager
|
||||
|
||||
本地化管理器接口,继承自 `ISystem`,提供本地化的核心功能。
|
||||
|
||||
**核心属性:**
|
||||
|
||||
```csharp
|
||||
string CurrentLanguage { get; } // 当前语言代码
|
||||
CultureInfo CurrentCulture { get; } // 当前文化信息
|
||||
IReadOnlyList<string> AvailableLanguages { get; } // 可用语言列表
|
||||
```
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
void SetLanguage(string languageCode); // 设置当前语言
|
||||
ILocalizationTable GetTable(string tableName); // 获取本地化表
|
||||
string GetText(string table, string key); // 获取本地化文本
|
||||
ILocalizationString GetString(string table, string key); // 获取本地化字符串(支持变量)
|
||||
bool TryGetText(string table, string key, out string text); // 尝试获取文本
|
||||
void RegisterFormatter(string name, ILocalizationFormatter formatter); // 注册格式化器
|
||||
void SubscribeToLanguageChange(Action<string> callback); // 订阅语言变化
|
||||
void UnsubscribeFromLanguageChange(Action<string> callback); // 取消订阅
|
||||
```
|
||||
|
||||
### ILocalizationTable
|
||||
|
||||
本地化表接口,表示单个语言的本地化数据表。
|
||||
|
||||
**核心属性:**
|
||||
|
||||
```csharp
|
||||
string Name { get; } // 表名
|
||||
string Language { get; } // 语言代码
|
||||
ILocalizationTable? Fallback { get; } // 回退表
|
||||
```
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
string GetRawText(string key); // 获取原始文本
|
||||
bool ContainsKey(string key); // 检查键是否存在
|
||||
IEnumerable<string> GetKeys(); // 获取所有键
|
||||
void Merge(IReadOnlyDictionary<string, string> overrides); // 合并覆盖数据
|
||||
```
|
||||
|
||||
### ILocalizationString
|
||||
|
||||
本地化字符串接口,支持变量替换和格式化。
|
||||
|
||||
**核心属性:**
|
||||
|
||||
```csharp
|
||||
string Table { get; } // 表名
|
||||
string Key { get; } // 键名
|
||||
```
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
ILocalizationString WithVariable(string name, object value); // 添加变量
|
||||
ILocalizationString WithVariables(params (string name, object value)[] variables); // 批量添加变量
|
||||
string Format(); // 格式化并返回文本
|
||||
string GetRaw(); // 获取原始文本
|
||||
bool Exists(); // 检查键是否存在
|
||||
```
|
||||
|
||||
### ILocalizationFormatter
|
||||
|
||||
格式化器接口,用于自定义变量格式化逻辑。
|
||||
|
||||
**核心属性:**
|
||||
|
||||
```csharp
|
||||
string Name { get; } // 格式化器名称
|
||||
```
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
bool TryFormat(string format, object value, IFormatProvider? provider, out string result);
|
||||
```
|
||||
|
||||
## 配置类
|
||||
|
||||
### LocalizationConfig
|
||||
|
||||
本地化配置类,用于配置本地化系统的行为。
|
||||
|
||||
**配置属性:**
|
||||
|
||||
```csharp
|
||||
string DefaultLanguage { get; set; } // 默认语言代码,默认 "eng"
|
||||
string FallbackLanguage { get; set; } // 回退语言代码,默认 "eng"
|
||||
string LocalizationPath { get; set; } // 本地化文件路径,默认 "res://localization"
|
||||
string OverridePath { get; set; } // 用户覆盖路径,默认 "user://localization_override" (暂不支持)
|
||||
bool EnableHotReload { get; set; } // 是否启用热重载,默认 true (暂不支持)
|
||||
bool ValidateOnLoad { get; set; } // 是否在加载时验证,默认 true (暂不支持)
|
||||
```
|
||||
|
||||
**注意:** `OverridePath`、`EnableHotReload` 和 `ValidateOnLoad` 配置项已定义但当前版本暂不支持,将在后续版本中实现。
|
||||
|
||||
## 文件组织
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
res://localization/
|
||||
├── eng/ # 英文
|
||||
│ ├── common.json # 通用文本
|
||||
│ ├── ui.json # UI 文本
|
||||
│ ├── cards.json # 卡牌文本
|
||||
│ └── ...
|
||||
├── zhs/ # 简体中文
|
||||
│ ├── common.json
|
||||
│ ├── ui.json
|
||||
│ └── ...
|
||||
└── ...
|
||||
|
||||
user://localization_override/ # 用户覆盖(可选)
|
||||
├── eng/
|
||||
└── zhs/
|
||||
```
|
||||
|
||||
### JSON 文件格式
|
||||
|
||||
```json
|
||||
{
|
||||
"game.title": "My Game",
|
||||
"game.version": "Version {version}",
|
||||
"ui.button.start": "Start Game",
|
||||
"ui.message.welcome": "Welcome, {playerName}!",
|
||||
"combat.damage": "Deal {damage} damage",
|
||||
"status.health": "Health: {current}/{max}"
|
||||
}
|
||||
```
|
||||
|
||||
**命名约定:**
|
||||
|
||||
- 使用点号分隔的层级结构(如 `ui.button.start`)
|
||||
- 变量使用花括号包裹(如 `{playerName}`)
|
||||
- 键名使用小写字母和点号
|
||||
|
||||
## 基本使用
|
||||
|
||||
### 初始化本地化管理器
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Localization;
|
||||
using GFramework.Core.Localization;
|
||||
|
||||
// 创建配置
|
||||
var config = new LocalizationConfig
|
||||
{
|
||||
DefaultLanguage = "eng",
|
||||
FallbackLanguage = "eng",
|
||||
LocalizationPath = "res://localization"
|
||||
};
|
||||
|
||||
// 创建管理器
|
||||
var locManager = new LocalizationManager(config);
|
||||
locManager.Initialize();
|
||||
```
|
||||
|
||||
### 在 Architecture 中注册
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture<GameArchitecture>
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 注册本地化管理器
|
||||
this.RegisterSystem<ILocalizationManager>(new LocalizationManager());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 获取本地化文本
|
||||
|
||||
```csharp
|
||||
// 获取管理器
|
||||
var locManager = this.GetSystem<ILocalizationManager>();
|
||||
|
||||
// 简单文本
|
||||
string title = locManager.GetText("common", "game.title");
|
||||
// 结果: "My Game"
|
||||
|
||||
// 安全获取
|
||||
if (locManager.TryGetText("common", "game.title", out var text))
|
||||
{
|
||||
Debug.Log(text);
|
||||
}
|
||||
```
|
||||
|
||||
### 使用变量
|
||||
|
||||
```csharp
|
||||
// 单个变量
|
||||
var message = locManager.GetString("common", "ui.message.welcome")
|
||||
.WithVariable("playerName", "Alice")
|
||||
.Format();
|
||||
// 结果: "Welcome, Alice!"
|
||||
|
||||
// 多个变量
|
||||
var health = locManager.GetString("common", "status.health")
|
||||
.WithVariable("current", 80)
|
||||
.WithVariable("max", 100)
|
||||
.Format();
|
||||
// 结果: "Health: 80/100"
|
||||
|
||||
// 链式调用
|
||||
var text = locManager.GetString("common", "game.version")
|
||||
.WithVariable("version", "1.0.0")
|
||||
.Format();
|
||||
// 结果: "Version 1.0.0"
|
||||
```
|
||||
|
||||
### 切换语言
|
||||
|
||||
```csharp
|
||||
// 切换到简体中文
|
||||
locManager.SetLanguage("zhs");
|
||||
|
||||
// 获取文本(自动使用新语言)
|
||||
string title = locManager.GetText("common", "game.title");
|
||||
// 结果: "我的游戏"
|
||||
|
||||
// 获取当前语言
|
||||
string currentLang = locManager.CurrentLanguage; // "zhs"
|
||||
|
||||
// 获取可用语言列表
|
||||
var languages = locManager.AvailableLanguages;
|
||||
foreach (var lang in languages)
|
||||
{
|
||||
Debug.Log(lang); // "eng", "zhs", ...
|
||||
}
|
||||
```
|
||||
|
||||
### 监听语言变化
|
||||
|
||||
```csharp
|
||||
// 方式 1: 使用 lambda 表达式(无法取消订阅)
|
||||
locManager.SubscribeToLanguageChange(language =>
|
||||
{
|
||||
Debug.Log($"Language changed to: {language}");
|
||||
// 更新 UI、重新加载资源等
|
||||
});
|
||||
|
||||
// 方式 2: 使用命名方法(推荐,可以取消订阅)
|
||||
void OnLanguageChanged(string language)
|
||||
{
|
||||
Debug.Log($"Language changed to: {language}");
|
||||
// 更新 UI、重新加载资源等
|
||||
}
|
||||
|
||||
// 订阅
|
||||
locManager.SubscribeToLanguageChange(OnLanguageChanged);
|
||||
|
||||
// 取消订阅(使用相同的方法引用)
|
||||
locManager.UnsubscribeFromLanguageChange(OnLanguageChanged);
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 回退机制
|
||||
|
||||
当目标语言缺少某个键时,系统会自动回退到默认语言:
|
||||
|
||||
```csharp
|
||||
// 假设 zhs/common.json 中缺少 "new.feature" 键
|
||||
locManager.SetLanguage("zhs");
|
||||
var text = locManager.GetText("common", "new.feature");
|
||||
// 自动从 eng/common.json 获取
|
||||
|
||||
// 回退顺序:
|
||||
// 1. 当前语言的覆盖数据
|
||||
// 2. 当前语言的原始数据
|
||||
// 3. 回退语言的数据
|
||||
```
|
||||
|
||||
### 覆盖机制
|
||||
|
||||
**注意:** 覆盖机制功能已规划但当前版本暂不支持,将在后续版本中实现。
|
||||
|
||||
未来版本中,用户可以在 `user://localization_override/` 目录下放置覆盖文件:
|
||||
|
||||
```json
|
||||
// user://localization_override/eng/common.json
|
||||
{
|
||||
"game.title": "My Custom Game Title"
|
||||
}
|
||||
```
|
||||
|
||||
覆盖文件会自动合并到主本地化表中,优先级最高。
|
||||
|
||||
### 自定义格式化器
|
||||
|
||||
```csharp
|
||||
// 实现自定义格式化器
|
||||
public class UpperCaseFormatter : ILocalizationFormatter
|
||||
{
|
||||
public string Name => "upper";
|
||||
|
||||
public bool TryFormat(string format, object value, IFormatProvider? provider, out string result)
|
||||
{
|
||||
result = value?.ToString()?.ToUpper() ?? string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册格式化器
|
||||
locManager.RegisterFormatter("upper", new UpperCaseFormatter());
|
||||
|
||||
// 使用格式化器(需要在 LocalizationString 中实现格式化器支持)
|
||||
// 格式: {variableName:formatterName:args}
|
||||
```
|
||||
|
||||
### 内置格式化器
|
||||
|
||||
#### ConditionalFormatter
|
||||
|
||||
条件格式化器,根据布尔值选择不同文本。
|
||||
|
||||
```csharp
|
||||
// 格式: {condition:if:trueText|falseText}
|
||||
// JSON: "status": "{upgraded:if:Upgraded|Normal}"
|
||||
|
||||
var text = locManager.GetString("common", "status")
|
||||
.WithVariable("upgraded", true)
|
||||
.Format();
|
||||
// 结果: "Upgraded"
|
||||
```
|
||||
|
||||
#### PluralFormatter
|
||||
|
||||
复数格式化器,根据数量选择单复数形式。
|
||||
|
||||
```csharp
|
||||
// 格式: {count:plural:singular|plural}
|
||||
// JSON: "items": "{count:plural:item|items}"
|
||||
|
||||
var text = locManager.GetString("common", "items")
|
||||
.WithVariable("count", 1)
|
||||
.Format();
|
||||
// 结果: "item"
|
||||
|
||||
var text2 = locManager.GetString("common", "items")
|
||||
.WithVariable("count", 3)
|
||||
.Format();
|
||||
// 结果: "items"
|
||||
```
|
||||
|
||||
## 异常处理
|
||||
|
||||
### LocalizationException
|
||||
|
||||
本地化异常基类。
|
||||
|
||||
### LocalizationKeyNotFoundException
|
||||
|
||||
当请求的键不存在时抛出。
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
var text = locManager.GetText("common", "nonexistent.key");
|
||||
}
|
||||
catch (LocalizationKeyNotFoundException ex)
|
||||
{
|
||||
Debug.LogError($"Key not found: {ex.TableName}.{ex.Key}");
|
||||
}
|
||||
```
|
||||
|
||||
### LocalizationTableNotFoundException
|
||||
|
||||
当请求的表不存在时抛出。
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
var table = locManager.GetTable("nonexistent_table");
|
||||
}
|
||||
catch (LocalizationTableNotFoundException ex)
|
||||
{
|
||||
Debug.LogError($"Table not found: {ex.TableName}");
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 键名组织
|
||||
|
||||
```csharp
|
||||
// 推荐:使用层级结构
|
||||
"ui.button.start"
|
||||
"ui.button.quit"
|
||||
"combat.damage.physical"
|
||||
"combat.damage.magical"
|
||||
|
||||
// 不推荐:扁平结构
|
||||
"start_button"
|
||||
"quit_button"
|
||||
```
|
||||
|
||||
### 2. 变量命名
|
||||
|
||||
```csharp
|
||||
// 推荐:使用驼峰命名
|
||||
"{playerName}"
|
||||
"{maxHealth}"
|
||||
|
||||
// 不推荐:使用下划线或大写
|
||||
"{player_name}"
|
||||
"{MAX_HEALTH}"
|
||||
```
|
||||
|
||||
### 3. 表的划分
|
||||
|
||||
```csharp
|
||||
// 按功能模块划分表
|
||||
common.json // 通用文本
|
||||
ui.json // UI 文本
|
||||
combat.json // 战斗文本
|
||||
items.json // 物品文本
|
||||
```
|
||||
|
||||
### 4. 安全获取
|
||||
|
||||
```csharp
|
||||
// 推荐:使用 TryGetText 避免异常
|
||||
if (locManager.TryGetText("common", "key", out var text))
|
||||
{
|
||||
// 使用 text
|
||||
}
|
||||
|
||||
// 或者提供默认值
|
||||
var text = locManager.TryGetText("common", "key", out var result)
|
||||
? result
|
||||
: "Default Text";
|
||||
```
|
||||
|
||||
### 5. 语言变化处理
|
||||
|
||||
```csharp
|
||||
// 在组件初始化时订阅
|
||||
public override void OnInit()
|
||||
{
|
||||
var locManager = this.GetSystem<ILocalizationManager>();
|
||||
locManager.SubscribeToLanguageChange(OnLanguageChanged);
|
||||
}
|
||||
|
||||
// 在组件销毁时取消订阅
|
||||
public override void OnDestroy()
|
||||
{
|
||||
var locManager = this.GetSystem<ILocalizationManager>();
|
||||
locManager.UnsubscribeFromLanguageChange(OnLanguageChanged);
|
||||
}
|
||||
|
||||
private void OnLanguageChanged(string language)
|
||||
{
|
||||
// 更新 UI
|
||||
UpdateUI();
|
||||
}
|
||||
```
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 缓存策略
|
||||
|
||||
- 本地化表在加载后会缓存在内存中
|
||||
- 语言切换时只加载新语言的表
|
||||
- 建议在游戏启动时预加载常用语言
|
||||
|
||||
### 内存优化
|
||||
|
||||
```csharp
|
||||
// 只加载当前语言,不预加载所有语言
|
||||
var config = new LocalizationConfig
|
||||
{
|
||||
DefaultLanguage = "eng"
|
||||
};
|
||||
|
||||
// 按需切换语言
|
||||
locManager.SetLanguage(userSelectedLanguage);
|
||||
```
|
||||
|
||||
## 相关资源
|
||||
|
||||
- [Architecture 架构系统](./architecture.md)
|
||||
- [System 系统层](./system.md)
|
||||
- [Configuration 配置管理](./configuration.md)
|
||||
@ -100,8 +100,8 @@ void ClearAll();
|
||||
void RegisterHandler(IPauseHandler handler);
|
||||
void UnregisterHandler(IPauseHandler handler);
|
||||
|
||||
// 状态变化事件
|
||||
event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||
// 状态变化事件
|
||||
event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
@ -377,16 +377,16 @@ public partial class PauseIndicator : IController
|
||||
_pauseManager.OnPauseStateChanged += OnPauseStateChanged;
|
||||
}
|
||||
|
||||
private void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
Console.WriteLine($"暂停状态变化: 组={group}, 暂停={isPaused}");
|
||||
|
||||
if (group == PauseGroup.Global)
|
||||
{
|
||||
if (isPaused)
|
||||
{
|
||||
ShowPauseIndicator();
|
||||
}
|
||||
private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e)
|
||||
{
|
||||
Console.WriteLine($"暂停状态变化: 组={e.Group}, 暂停={e.IsPaused}");
|
||||
|
||||
if (e.Group == PauseGroup.Global)
|
||||
{
|
||||
if (e.IsPaused)
|
||||
{
|
||||
ShowPauseIndicator();
|
||||
}
|
||||
else
|
||||
{
|
||||
HidePauseIndicator();
|
||||
@ -705,7 +705,7 @@ public partial class ProperCleanup : IController
|
||||
_pauseManager.OnPauseStateChanged -= OnPauseChanged;
|
||||
}
|
||||
|
||||
private void OnPauseChanged(PauseGroup group, bool isPaused) { }
|
||||
private void OnPauseChanged(object? sender, PauseStateChangedEventArgs e) { }
|
||||
}
|
||||
```
|
||||
|
||||
@ -743,13 +743,13 @@ public partial class PauseMenu : Control
|
||||
|
||||
// 方案 2: 监听暂停事件
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
pauseManager.OnPauseStateChanged += (group, isPaused) =>
|
||||
{
|
||||
if (group == PauseGroup.Global)
|
||||
{
|
||||
Visible = isPaused;
|
||||
}
|
||||
};
|
||||
pauseManager.OnPauseStateChanged += (_, e) =>
|
||||
{
|
||||
if (e.Group == PauseGroup.Global)
|
||||
{
|
||||
Visible = e.IsPaused;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -887,15 +887,15 @@ public class PauseEventBridge : AbstractSystem
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
pauseManager.OnPauseStateChanged += (group, isPaused) =>
|
||||
{
|
||||
// 发送暂停事件
|
||||
this.SendEvent(new GamePausedEvent
|
||||
{
|
||||
Group = group,
|
||||
IsPaused = isPaused
|
||||
});
|
||||
};
|
||||
pauseManager.OnPauseStateChanged += (_, e) =>
|
||||
{
|
||||
// 发送暂停事件
|
||||
this.SendEvent(new GamePausedEvent
|
||||
{
|
||||
Group = e.Group,
|
||||
IsPaused = e.IsPaused
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -365,6 +365,42 @@ public class EncryptionUtility : IUtility
|
||||
}
|
||||
```
|
||||
|
||||
### 内置数值显示工具
|
||||
|
||||
对于 UI 中常见的数值缩写显示,优先使用 `GFramework.Core` 提供的数值显示工具,而不是在业务层重复拼接字符串。
|
||||
|
||||
```csharp
|
||||
using System.Globalization;
|
||||
using GFramework.Core.Abstractions.Utility.Numeric;
|
||||
using GFramework.Core.Extensions;
|
||||
using GFramework.Core.Utility.Numeric;
|
||||
|
||||
var gold = NumericDisplay.FormatCompact(1250); // "1.3K"
|
||||
var damage = 15320.ToCompactString(); // "15.3K"
|
||||
|
||||
var exact = NumericDisplay.Format(1234.56m, new NumericFormatOptions
|
||||
{
|
||||
MaxDecimalPlaces = 2,
|
||||
FormatProvider = CultureInfo.InvariantCulture
|
||||
}); // "1.23K"
|
||||
|
||||
var grouped = NumericDisplay.Format(12345, new NumericFormatOptions
|
||||
{
|
||||
CompactThreshold = 1000000m,
|
||||
UseGroupingBelowThreshold = true,
|
||||
FormatProvider = CultureInfo.InvariantCulture
|
||||
}); // "12,345"
|
||||
```
|
||||
|
||||
如果你在本地化文本中展示数值,也可以直接使用内置 formatter:
|
||||
|
||||
```json
|
||||
{
|
||||
"status.gold": "Gold: {gold:compact}",
|
||||
"status.damage": "Damage: {damage:compact:maxDecimals=2}"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 对象池工具
|
||||
|
||||
```csharp
|
||||
@ -610,4 +646,4 @@ public class CollectionUtility : IUtility
|
||||
- [`command`](./command.md) - Command 中可以使用 Utility
|
||||
- [`architecture`](./architecture.md) - 在架构中注册 Utility
|
||||
- [`ioc`](./ioc.md) - Utility 通过 IoC 容器管理
|
||||
- [`extensions`](./extensions.md) - 提供 GetUtility 扩展方法
|
||||
- [`extensions`](./extensions.md) - 提供 GetUtility 扩展方法
|
||||
|
||||
@ -44,7 +44,7 @@ public interface IPauseStackManager : IContextUtility
|
||||
int GetPauseDepth(PauseGroup group = PauseGroup.Global);
|
||||
|
||||
// 暂停状态变化事件
|
||||
event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||
event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
|
||||
}
|
||||
```
|
||||
|
||||
@ -477,12 +477,12 @@ public partial class PauseIndicator : Label
|
||||
pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
|
||||
}
|
||||
|
||||
private void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e)
|
||||
{
|
||||
if (group == PauseGroup.Global)
|
||||
if (e.Group == PauseGroup.Global)
|
||||
{
|
||||
Text = isPaused ? "游戏已暂停" : "游戏运行中";
|
||||
Visible = isPaused;
|
||||
Text = e.IsPaused ? "游戏已暂停" : "游戏运行中";
|
||||
Visible = e.IsPaused;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -502,16 +502,16 @@ public partial class PauseDebugger : Node
|
||||
pauseManager.OnPauseStateChanged += OnPauseStateChanged;
|
||||
}
|
||||
|
||||
private void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e)
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
GD.Print($"=== 暂停状态变化 ===");
|
||||
GD.Print($"组: {group}");
|
||||
GD.Print($"状态: {(isPaused ? "暂停" : "恢复")}");
|
||||
GD.Print($"深度: {pauseManager.GetPauseDepth(group)}");
|
||||
GD.Print($"组: {e.Group}");
|
||||
GD.Print($"状态: {(e.IsPaused ? "暂停" : "恢复")}");
|
||||
GD.Print($"深度: {pauseManager.GetPauseDepth(e.Group)}");
|
||||
|
||||
var reasons = pauseManager.GetPauseReasons(group);
|
||||
var reasons = pauseManager.GetPauseReasons(e.Group);
|
||||
if (reasons.Count > 0)
|
||||
{
|
||||
GD.Print($"原因:");
|
||||
@ -609,9 +609,9 @@ public partial class PauseDebugger : Node
|
||||
|
||||
7. **使用事件监听暂停状态**:实现响应式 UI
|
||||
```csharp
|
||||
pauseManager.OnPauseStateChanged += (group, isPaused) =>
|
||||
pauseManager.OnPauseStateChanged += (_, e) =>
|
||||
{
|
||||
UpdateUI(isPaused);
|
||||
UpdateUI(e.IsPaused);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
@ -189,7 +189,7 @@ public class GameArchitecture : Architecture
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 使用 IOC 容器注册
|
||||
// ✅ 使用 IoC 容器注册
|
||||
protected override void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IPlayerService, PlayerService>();
|
||||
|
||||
1
packaging/_._
Normal file
1
packaging/_._
Normal file
@ -0,0 +1 @@
|
||||
|
||||
268
scripts/collect-dev-environment.sh
Normal file
268
scripts/collect-dev-environment.sh
Normal file
@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
OUTPUT_PATH="${ROOT_DIR}/.ai/environment/tools.raw.yaml"
|
||||
MODE="${1:---check}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
bash scripts/collect-dev-environment.sh --check
|
||||
bash scripts/collect-dev-environment.sh --write
|
||||
|
||||
Modes:
|
||||
--check Print the raw project-relevant environment inventory.
|
||||
--write Write the raw inventory to .ai/environment/tools.raw.yaml.
|
||||
EOF
|
||||
}
|
||||
|
||||
ensure_supported_mode() {
|
||||
case "${MODE}" in
|
||||
--check|--write)
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
command_path() {
|
||||
local tool="$1"
|
||||
|
||||
if command -v "${tool}" >/dev/null 2>&1; then
|
||||
command -v "${tool}"
|
||||
else
|
||||
printf '%s' ""
|
||||
fi
|
||||
}
|
||||
|
||||
command_installed() {
|
||||
local tool="$1"
|
||||
|
||||
if command -v "${tool}" >/dev/null 2>&1; then
|
||||
printf 'true'
|
||||
else
|
||||
printf 'false'
|
||||
fi
|
||||
}
|
||||
|
||||
command_version() {
|
||||
local tool="$1"
|
||||
|
||||
if ! command -v "${tool}" >/dev/null 2>&1; then
|
||||
printf '%s' "not-installed"
|
||||
return
|
||||
fi
|
||||
|
||||
case "${tool}" in
|
||||
dotnet)
|
||||
dotnet --version 2>/dev/null || printf '%s' "unknown"
|
||||
;;
|
||||
python3)
|
||||
python3 --version 2>/dev/null || printf '%s' "unknown"
|
||||
;;
|
||||
node)
|
||||
node --version 2>/dev/null || printf '%s' "unknown"
|
||||
;;
|
||||
npm)
|
||||
npm --version 2>/dev/null || printf '%s' "unknown"
|
||||
;;
|
||||
bun)
|
||||
bun --version 2>/dev/null || printf '%s' "unknown"
|
||||
;;
|
||||
git)
|
||||
git --version 2>/dev/null || printf '%s' "unknown"
|
||||
;;
|
||||
rg)
|
||||
rg --version 2>/dev/null | head -n 1 || printf '%s' "unknown"
|
||||
;;
|
||||
jq)
|
||||
jq --version 2>/dev/null || printf '%s' "unknown"
|
||||
;;
|
||||
docker)
|
||||
docker --version 2>/dev/null || printf '%s' "unknown"
|
||||
;;
|
||||
bash)
|
||||
bash --version 2>/dev/null | head -n 1 || printf '%s' "unknown"
|
||||
;;
|
||||
*)
|
||||
"${tool}" --version 2>/dev/null | head -n 1 || printf '%s' "unknown"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
python_package_version() {
|
||||
local package_name="$1"
|
||||
|
||||
python3 - "${package_name}" <<'PY'
|
||||
from importlib import metadata
|
||||
import sys
|
||||
|
||||
package_name = sys.argv[1]
|
||||
|
||||
try:
|
||||
print(metadata.version(package_name))
|
||||
except metadata.PackageNotFoundError:
|
||||
print("not-installed")
|
||||
PY
|
||||
}
|
||||
|
||||
python_package_installed() {
|
||||
local package_name="$1"
|
||||
local version
|
||||
|
||||
version="$(python_package_version "${package_name}")"
|
||||
|
||||
if [[ "${version}" == "not-installed" ]]; then
|
||||
printf 'false'
|
||||
else
|
||||
printf 'true'
|
||||
fi
|
||||
}
|
||||
|
||||
read_os_release() {
|
||||
local key="$1"
|
||||
|
||||
python3 - "$key" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
target_key = sys.argv[1]
|
||||
values = {}
|
||||
for line in pathlib.Path("/etc/os-release").read_text(encoding="utf-8").splitlines():
|
||||
if "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
values[key] = value.strip().strip('"')
|
||||
|
||||
print(values.get(target_key, "unknown"))
|
||||
PY
|
||||
}
|
||||
|
||||
collect_inventory() {
|
||||
local os_name distro version_id kernel shell_name wsl_enabled wsl_version timestamp
|
||||
|
||||
os_name="$(uname -s)"
|
||||
distro="$(read_os_release PRETTY_NAME)"
|
||||
version_id="$(read_os_release VERSION_ID)"
|
||||
kernel="$(uname -r)"
|
||||
shell_name="$(basename "${SHELL:-bash}")"
|
||||
timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
|
||||
if grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
wsl_enabled="true"
|
||||
else
|
||||
wsl_enabled="false"
|
||||
fi
|
||||
|
||||
if command -v wslinfo >/dev/null 2>&1; then
|
||||
wsl_version="$(wslinfo --wsl-version 2>/dev/null || printf '%s' "unknown")"
|
||||
else
|
||||
wsl_version="unknown"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
schema_version: 1
|
||||
generated_at_utc: "${timestamp}"
|
||||
generator: "scripts/collect-dev-environment.sh"
|
||||
|
||||
platform:
|
||||
os: "${os_name}"
|
||||
distro: "${distro}"
|
||||
version: "${version_id}"
|
||||
kernel: "${kernel}"
|
||||
wsl: ${wsl_enabled}
|
||||
wsl_version: "${wsl_version}"
|
||||
shell: "${shell_name}"
|
||||
|
||||
required_runtimes:
|
||||
dotnet:
|
||||
installed: $(command_installed dotnet)
|
||||
version: "$(command_version dotnet)"
|
||||
path: "$(command_path dotnet)"
|
||||
purpose: "Builds and tests the GFramework solution."
|
||||
python3:
|
||||
installed: $(command_installed python3)
|
||||
version: "$(command_version python3)"
|
||||
path: "$(command_path python3)"
|
||||
purpose: "Runs local automation and environment collection scripts."
|
||||
node:
|
||||
installed: $(command_installed node)
|
||||
version: "$(command_version node)"
|
||||
path: "$(command_path node)"
|
||||
purpose: "Provides the JavaScript runtime used by docs tooling."
|
||||
bun:
|
||||
installed: $(command_installed bun)
|
||||
version: "$(command_version bun)"
|
||||
path: "$(command_path bun)"
|
||||
purpose: "Installs and previews the VitePress documentation site."
|
||||
|
||||
required_tools:
|
||||
git:
|
||||
installed: $(command_installed git)
|
||||
version: "$(command_version git)"
|
||||
path: "$(command_path git)"
|
||||
purpose: "Source control and patch review."
|
||||
bash:
|
||||
installed: $(command_installed bash)
|
||||
version: "$(command_version bash)"
|
||||
path: "$(command_path bash)"
|
||||
purpose: "Executes repository scripts and shell automation."
|
||||
rg:
|
||||
installed: $(command_installed rg)
|
||||
version: "$(command_version rg)"
|
||||
path: "$(command_path rg)"
|
||||
purpose: "Fast text search across the repository."
|
||||
jq:
|
||||
installed: $(command_installed jq)
|
||||
version: "$(command_version jq)"
|
||||
path: "$(command_path jq)"
|
||||
purpose: "Inspecting and transforming JSON outputs."
|
||||
|
||||
project_tools:
|
||||
docker:
|
||||
installed: $(command_installed docker)
|
||||
version: "$(command_version docker)"
|
||||
path: "$(command_path docker)"
|
||||
purpose: "Runs MegaLinter and other containerized validation tools."
|
||||
|
||||
python_packages:
|
||||
requests:
|
||||
installed: $(python_package_installed requests)
|
||||
version: "$(python_package_version requests)"
|
||||
purpose: "Simple HTTP calls in local helper scripts."
|
||||
rich:
|
||||
installed: $(python_package_installed rich)
|
||||
version: "$(python_package_version rich)"
|
||||
purpose: "Readable CLI output for local Python helpers."
|
||||
openai:
|
||||
installed: $(python_package_installed openai)
|
||||
version: "$(python_package_version openai)"
|
||||
purpose: "Optional scripted access to OpenAI APIs."
|
||||
tiktoken:
|
||||
installed: $(python_package_installed tiktoken)
|
||||
version: "$(python_package_version tiktoken)"
|
||||
purpose: "Optional token counting for prompt and context inspection."
|
||||
pydantic:
|
||||
installed: $(python_package_installed pydantic)
|
||||
version: "$(python_package_version pydantic)"
|
||||
purpose: "Optional typed config and schema validation for helper scripts."
|
||||
pytest:
|
||||
installed: $(python_package_installed pytest)
|
||||
version: "$(python_package_version pytest)"
|
||||
purpose: "Optional lightweight testing for Python helper scripts."
|
||||
EOF
|
||||
}
|
||||
|
||||
ensure_supported_mode
|
||||
|
||||
if [[ "${MODE}" == "--write" ]]; then
|
||||
mkdir -p "$(dirname "${OUTPUT_PATH}")"
|
||||
collect_inventory > "${OUTPUT_PATH}"
|
||||
printf 'Wrote %s\n' "${OUTPUT_PATH}"
|
||||
else
|
||||
collect_inventory
|
||||
fi
|
||||
236
scripts/generate-ai-environment.py
Normal file
236
scripts/generate-ai-environment.py
Normal file
@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
RAW_PATH = ROOT_DIR / ".ai" / "environment" / "tools.raw.yaml"
|
||||
AI_PATH = ROOT_DIR / ".ai" / "environment" / "tools.ai.yaml"
|
||||
|
||||
|
||||
def parse_scalar(value: str) -> Any:
|
||||
if value == "true":
|
||||
return True
|
||||
if value == "false":
|
||||
return False
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
return value[1:-1]
|
||||
return value
|
||||
|
||||
|
||||
def parse_simple_yaml(path: Path) -> dict[str, Any]:
|
||||
root: dict[str, Any] = {}
|
||||
stack: list[tuple[int, dict[str, Any]]] = [(-1, root)]
|
||||
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
if not raw_line.strip():
|
||||
continue
|
||||
if raw_line.lstrip().startswith("#"):
|
||||
continue
|
||||
|
||||
indent = len(raw_line) - len(raw_line.lstrip(" "))
|
||||
key, _, tail = raw_line.strip().partition(":")
|
||||
|
||||
while len(stack) > 1 and indent <= stack[-1][0]:
|
||||
stack.pop()
|
||||
|
||||
current = stack[-1][1]
|
||||
value = tail.strip()
|
||||
|
||||
if value == "":
|
||||
child: dict[str, Any] = {}
|
||||
current[key] = child
|
||||
stack.append((indent, child))
|
||||
continue
|
||||
|
||||
current[key] = parse_scalar(value)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
def bool_value(data: dict[str, Any], *keys: str) -> bool:
|
||||
current: Any = data
|
||||
for key in keys:
|
||||
current = current[key]
|
||||
return bool(current)
|
||||
|
||||
|
||||
def string_value(data: dict[str, Any], *keys: str) -> str:
|
||||
current: Any = data
|
||||
for key in keys:
|
||||
current = current[key]
|
||||
return str(current)
|
||||
|
||||
|
||||
def choose(preferred: str | None, fallback: str | None) -> str:
|
||||
if preferred:
|
||||
return preferred
|
||||
return fallback or "unavailable"
|
||||
|
||||
|
||||
def available_tool(raw: dict[str, Any], section: str, name: str) -> bool:
|
||||
return bool_value(raw, section, name, "installed")
|
||||
|
||||
|
||||
def select_tool(
|
||||
use_for: str,
|
||||
preferred: str | None,
|
||||
fallback: str | None,
|
||||
) -> dict[str, str]:
|
||||
return {
|
||||
"preferred": choose(preferred, fallback),
|
||||
"fallback": fallback or "unavailable",
|
||||
"use_for": use_for,
|
||||
}
|
||||
|
||||
|
||||
def build_ai_inventory(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
has_python = available_tool(raw, "required_runtimes", "python3")
|
||||
has_node = available_tool(raw, "required_runtimes", "node")
|
||||
has_bun = available_tool(raw, "required_runtimes", "bun")
|
||||
has_dotnet = available_tool(raw, "required_runtimes", "dotnet")
|
||||
has_rg = available_tool(raw, "required_tools", "rg")
|
||||
has_jq = available_tool(raw, "required_tools", "jq")
|
||||
has_bash = available_tool(raw, "required_tools", "bash")
|
||||
has_docker = available_tool(raw, "project_tools", "docker")
|
||||
|
||||
search = select_tool(
|
||||
use_for="Repository text search.",
|
||||
preferred="rg" if has_rg else None,
|
||||
fallback="grep",
|
||||
)
|
||||
json = select_tool(
|
||||
use_for="Inspecting or transforming JSON command output.",
|
||||
preferred="jq" if has_jq else None,
|
||||
fallback="python3" if has_python else None,
|
||||
)
|
||||
scripting = select_tool(
|
||||
use_for="Non-trivial local automation and helper scripts.",
|
||||
preferred="python3" if has_python else None,
|
||||
fallback="bash" if has_bash else None,
|
||||
)
|
||||
shell = select_tool(
|
||||
use_for="Repository shell scripts and command execution.",
|
||||
preferred="bash" if has_bash else None,
|
||||
fallback="sh",
|
||||
)
|
||||
docs = select_tool(
|
||||
use_for="Installing and previewing the docs site.",
|
||||
preferred="bun" if has_bun else None,
|
||||
fallback="npm" if has_node else None,
|
||||
)
|
||||
build = select_tool(
|
||||
use_for="Build, test, restore, and solution validation.",
|
||||
preferred="dotnet" if has_dotnet else None,
|
||||
fallback=None,
|
||||
)
|
||||
|
||||
if bool_value(raw, "platform", "wsl"):
|
||||
platform_family = "wsl-linux"
|
||||
else:
|
||||
platform_family = string_value(raw, "platform", "os").lower()
|
||||
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"generated_at_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"generated_from": ".ai/environment/tools.raw.yaml",
|
||||
"generator": "scripts/generate-ai-environment.py",
|
||||
"platform": {
|
||||
"family": platform_family,
|
||||
"os": string_value(raw, "platform", "os"),
|
||||
"distro": string_value(raw, "platform", "distro"),
|
||||
"shell": string_value(raw, "platform", "shell"),
|
||||
},
|
||||
"capabilities": {
|
||||
"dotnet": has_dotnet,
|
||||
"python": has_python,
|
||||
"node": has_node,
|
||||
"bun": has_bun,
|
||||
"docker": has_docker,
|
||||
"fast_search": has_rg,
|
||||
"json_cli": has_jq,
|
||||
},
|
||||
"tool_selection": {
|
||||
"search": search,
|
||||
"json": json,
|
||||
"shell": shell,
|
||||
"scripting": scripting,
|
||||
"docs_package_manager": docs,
|
||||
"build_and_test": build,
|
||||
},
|
||||
"python": {
|
||||
"available": has_python,
|
||||
"helper_packages": {
|
||||
"requests": bool_value(raw, "python_packages", "requests", "installed"),
|
||||
"rich": bool_value(raw, "python_packages", "rich", "installed"),
|
||||
"openai": bool_value(raw, "python_packages", "openai", "installed"),
|
||||
"tiktoken": bool_value(raw, "python_packages", "tiktoken", "installed"),
|
||||
"pydantic": bool_value(raw, "python_packages", "pydantic", "installed"),
|
||||
"pytest": bool_value(raw, "python_packages", "pytest", "installed"),
|
||||
},
|
||||
},
|
||||
"preferences": {
|
||||
"prefer_project_listed_tools": True,
|
||||
"prefer_python_for_non_trivial_automation": has_python,
|
||||
"avoid_unlisted_system_tools": True,
|
||||
},
|
||||
"rules": [
|
||||
"Use rg instead of grep for repository search when rg is available.",
|
||||
"Use jq for JSON inspection; fall back to python3 if jq is unavailable.",
|
||||
"Prefer python3 over complex bash for non-trivial scripting when python3 is available.",
|
||||
"Use bun for docs preview workflows when bun is available; otherwise fall back to npm.",
|
||||
"Use dotnet for repository build and test workflows.",
|
||||
"Do not assume unrelated system tools are part of the supported project environment.",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def emit_yaml(value: Any, indent: int = 0) -> list[str]:
|
||||
prefix = " " * indent
|
||||
|
||||
if isinstance(value, dict):
|
||||
lines: list[str] = []
|
||||
for key, nested in value.items():
|
||||
if isinstance(nested, (dict, list)):
|
||||
lines.append(f"{prefix}{key}:")
|
||||
lines.extend(emit_yaml(nested, indent + 2))
|
||||
else:
|
||||
lines.append(f"{prefix}{key}: {format_scalar(nested)}")
|
||||
return lines
|
||||
|
||||
if isinstance(value, list):
|
||||
lines = []
|
||||
for item in value:
|
||||
if isinstance(item, (dict, list)):
|
||||
lines.append(f"{prefix}-")
|
||||
lines.extend(emit_yaml(item, indent + 2))
|
||||
else:
|
||||
lines.append(f"{prefix}- {format_scalar(item)}")
|
||||
return lines
|
||||
|
||||
return [f"{prefix}{format_scalar(value)}"]
|
||||
|
||||
|
||||
def format_scalar(value: Any) -> str:
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, int):
|
||||
return str(value)
|
||||
text = str(value).replace('"', '\\"')
|
||||
return f'"{text}"'
|
||||
|
||||
|
||||
def main() -> None:
|
||||
raw = parse_simple_yaml(RAW_PATH)
|
||||
ai_inventory = build_ai_inventory(raw)
|
||||
AI_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
AI_PATH.write_text("\n".join(emit_yaml(ai_inventory)) + "\n", encoding="utf-8")
|
||||
print(f"Wrote {AI_PATH}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user