mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-24 20:34:29 +08:00
Compare commits
No commits in common. "main" and "v0.0.203" have entirely different histories.
@ -1,62 +0,0 @@
|
|||||||
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."
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
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."
|
|
||||||
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
@ -13,9 +13,8 @@ permissions:
|
|||||||
security-events: write
|
security-events: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# 代码质量检查 job(并行执行,不阻塞构建)
|
test:
|
||||||
code-quality:
|
name: Build and Test
|
||||||
name: Code Quality & Security
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -24,11 +23,9 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# 校验C#命名空间与源码目录是否符合命名规范
|
# 校验C#命名空间与源码目录是否符合命名规范
|
||||||
- name: Validate C# naming
|
- name: Validate C# naming
|
||||||
run: bash scripts/validate-csharp-naming.sh
|
run: bash scripts/validate-csharp-naming.sh
|
||||||
|
|
||||||
# 缓存MegaLinter
|
# 缓存MegaLinter
|
||||||
- name: Cache MegaLinter
|
- name: Cache MegaLinter
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
@ -38,6 +35,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-megalinter-
|
${{ runner.os }}-megalinter-
|
||||||
|
|
||||||
|
|
||||||
# MegaLinter扫描步骤
|
# MegaLinter扫描步骤
|
||||||
# 执行代码质量检查和安全扫描,生成SARIF格式报告
|
# 执行代码质量检查和安全扫描,生成SARIF格式报告
|
||||||
- name: MegaLinter
|
- name: MegaLinter
|
||||||
@ -46,13 +44,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
FAIL_ON_ERROR: ${{ github.ref == 'refs/heads/main' }}
|
FAIL_ON_ERROR: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
# 上传SARIF格式的安全和代码质量问题报告到GitHub安全中心
|
# 上传SARIF格式的安全和代码质量问题报告到GitHub安全中心
|
||||||
- name: Upload SARIF
|
- name: Upload SARIF
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
uses: github/codeql-action/upload-sarif@v4
|
||||||
with:
|
with:
|
||||||
sarif_file: megalinter-reports/sarif
|
sarif_file: megalinter-reports/sarif
|
||||||
|
|
||||||
# 缓存TruffleHog
|
# 缓存TruffleHog
|
||||||
- name: Cache TruffleHog
|
- name: Cache TruffleHog
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
@ -73,18 +69,6 @@ jobs:
|
|||||||
# 当前提交哈希,作为扫描的目标版本
|
# 当前提交哈希,作为扫描的目标版本
|
||||||
head: ${{ github.sha }}
|
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版本
|
# 安装和配置.NET SDK版本
|
||||||
- name: Setup .NET 8
|
- name: Setup .NET 8
|
||||||
uses: actions/setup-dotnet@v5
|
uses: actions/setup-dotnet@v5
|
||||||
@ -129,35 +113,29 @@ jobs:
|
|||||||
run: dotnet build -c Release --no-restore
|
run: dotnet build -c Release --no-restore
|
||||||
|
|
||||||
# 运行单元测试,输出TRX格式结果到TestResults目录
|
# 运行单元测试,输出TRX格式结果到TestResults目录
|
||||||
# 在同一个 step 中并发执行所有测试以加快速度
|
- name: Test - Core
|
||||||
- name: Test All Projects
|
|
||||||
run: |
|
run: |
|
||||||
dotnet test GFramework.Core.Tests \
|
dotnet test GFramework.Core.Tests \
|
||||||
-c Release \
|
-c Release \
|
||||||
--no-build \
|
--no-build \
|
||||||
--logger "trx;LogFileName=core-$RANDOM.trx" \
|
--logger "trx;LogFileName=core-$RANDOM.trx" \
|
||||||
--results-directory TestResults &
|
--results-directory TestResults
|
||||||
|
|
||||||
dotnet test GFramework.Game.Tests \
|
|
||||||
-c Release \
|
|
||||||
--no-build \
|
|
||||||
--logger "trx;LogFileName=game-$RANDOM.trx" \
|
|
||||||
--results-directory TestResults &
|
|
||||||
|
|
||||||
|
- name: Test - SourceGenerators
|
||||||
|
run: |
|
||||||
dotnet test GFramework.SourceGenerators.Tests \
|
dotnet test GFramework.SourceGenerators.Tests \
|
||||||
-c Release \
|
-c Release \
|
||||||
--no-build \
|
--no-build \
|
||||||
--logger "trx;LogFileName=sg-$RANDOM.trx" \
|
--logger "trx;LogFileName=sg-$RANDOM.trx" \
|
||||||
--results-directory TestResults &
|
--results-directory TestResults
|
||||||
|
|
||||||
|
- name: Test - GFramework.Ecs.Arch.Tests
|
||||||
|
run: |
|
||||||
dotnet test GFramework.Ecs.Arch.Tests \
|
dotnet test GFramework.Ecs.Arch.Tests \
|
||||||
-c Release \
|
-c Release \
|
||||||
--no-build \
|
--no-build \
|
||||||
--logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \
|
--logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \
|
||||||
--results-directory TestResults &
|
--results-directory TestResults
|
||||||
|
|
||||||
# 等待所有后台测试完成
|
|
||||||
wait
|
|
||||||
|
|
||||||
- name: Generate CTRF report
|
- name: Generate CTRF report
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,5 +13,3 @@ opencode.json
|
|||||||
docs/.omc/
|
docs/.omc/
|
||||||
docs/.vitepress/cache/
|
docs/.vitepress/cache/
|
||||||
local-plan/
|
local-plan/
|
||||||
# tool
|
|
||||||
.venv/
|
|
||||||
220
AGENTS.md
220
AGENTS.md
@ -1,220 +0,0 @@
|
|||||||
# 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
134
CLAUDE.md
@ -1,134 +0,0 @@
|
|||||||
# 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 的设计重点不是把所有能力堆进单一核心类,而是通过清晰的模块边界、可组合的服务注册方式、稳定的抽象契约以及适度自动化的源码生成,构建一个适合长期演进的游戏开发基础框架。
|
|
||||||
@ -1,13 +1,13 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 架构模块注册表 - 用于外部模块的自动注册
|
/// 架构模块注册表 - 用于外部模块的自动注册
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ArchitectureModuleRegistry
|
public static class ArchitectureModuleRegistry
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<string, Func<IServiceModule>> Factories = new(StringComparer.Ordinal);
|
private static readonly ConcurrentDictionary<string, Func<IServiceModule>> _factories = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册模块工厂(幂等操作,相同模块名只会注册一次)
|
/// 注册模块工厂(幂等操作,相同模块名只会注册一次)
|
||||||
@ -20,7 +20,7 @@ public static class ArchitectureModuleRegistry
|
|||||||
var moduleName = tempModule.ModuleName;
|
var moduleName = tempModule.ModuleName;
|
||||||
|
|
||||||
// 幂等注册:相同模块名只注册一次
|
// 幂等注册:相同模块名只注册一次
|
||||||
Factories.TryAdd(moduleName, factory);
|
_factories.TryAdd(moduleName, factory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -29,7 +29,7 @@ public static class ArchitectureModuleRegistry
|
|||||||
/// <returns>模块实例集合</returns>
|
/// <returns>模块实例集合</returns>
|
||||||
public static IEnumerable<IServiceModule> CreateModules()
|
public static IEnumerable<IServiceModule> CreateModules()
|
||||||
{
|
{
|
||||||
return Factories.Values.Select(f => f());
|
return _factories.Values.Select(f => f());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -37,6 +37,6 @@ public static class ArchitectureModuleRegistry
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Clear()
|
public static void Clear()
|
||||||
{
|
{
|
||||||
Factories.Clear();
|
_factories.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,7 +4,7 @@ using GFramework.Core.Abstractions.Systems;
|
|||||||
using GFramework.Core.Abstractions.Utility;
|
using GFramework.Core.Abstractions.Utility;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 架构接口,专注于生命周期管理,包括系统、模型、工具的注册和获取
|
/// 架构接口,专注于生命周期管理,包括系统、模型、工具的注册和获取
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using GFramework.Core.Abstractions.Properties;
|
using GFramework.Core.Abstractions.Properties;
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 定义架构配置的接口,提供日志工厂、日志级别和架构选项的配置功能
|
/// 定义架构配置的接口,提供日志工厂、日志级别和架构选项的配置功能
|
||||||
@ -8,7 +8,7 @@ using GFramework.Core.Abstractions.Utility;
|
|||||||
using Mediator;
|
using Mediator;
|
||||||
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
|
using ICommand = GFramework.Core.Abstractions.Command.ICommand;
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 架构上下文接口,提供对系统、模型、工具类的访问以及命令、查询、事件的发送和注册功能
|
/// 架构上下文接口,提供对系统、模型、工具类的访问以及命令、查询、事件的发送和注册功能
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 架构上下文提供者接口,用于解耦上下文获取逻辑
|
/// 架构上下文提供者接口,用于解耦上下文获取逻辑
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 架构生命周期钩子接口,用于在架构的不同生命周期阶段执行自定义逻辑。
|
/// 架构生命周期钩子接口,用于在架构的不同生命周期阶段执行自定义逻辑。
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 架构模块接口,继承自架构生命周期接口。
|
/// 架构模块接口,继承自架构生命周期接口。
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 架构阶段监听器接口,用于监听和响应架构生命周期中的不同阶段变化。
|
/// 架构阶段监听器接口,用于监听和响应架构生命周期中的不同阶段变化。
|
||||||
@ -4,7 +4,7 @@ using GFramework.Core.Abstractions.Ioc;
|
|||||||
using GFramework.Core.Abstractions.Query;
|
using GFramework.Core.Abstractions.Query;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 架构服务接口,定义了框架核心架构所需的服务组件
|
/// 架构服务接口,定义了框架核心架构所需的服务组件
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using GFramework.Core.Abstractions.Ioc;
|
using GFramework.Core.Abstractions.Ioc;
|
||||||
using GFramework.Core.Abstractions.Lifecycle;
|
using GFramework.Core.Abstractions.Lifecycle;
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 服务模块接口,定义了服务模块的基本契约。
|
/// 服务模块接口,定义了服务模块的基本契约。
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using GFramework.Core.Abstractions.Ioc;
|
using GFramework.Core.Abstractions.Ioc;
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Architectures;
|
namespace GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 服务模块管理器接口,用于管理架构中的服务模块。
|
/// 服务模块管理器接口,用于管理架构中的服务模块。
|
||||||
@ -1,43 +0,0 @@
|
|||||||
// 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,14 +11,11 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Concurrency;
|
namespace GFramework.Core.Abstractions.Concurrency;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 锁统计信息
|
/// 锁统计信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[StructLayout(LayoutKind.Auto)]
|
|
||||||
public readonly struct LockStatistics
|
public readonly struct LockStatistics
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -41,3 +38,32 @@ public readonly struct LockStatistics
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int TotalCleaned { get; init; }
|
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"/>
|
<Using Include="GFramework.Core.Abstractions"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="Meziantou.Analyzer" Version="3.0.25">
|
<PackageReference Update="Meziantou.Analyzer" Version="3.0.19">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Lifecycle;
|
using GFramework.Core.Abstractions.Lifecycle;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
|
|
||||||
|
|||||||
@ -75,9 +75,7 @@ public interface IPauseStackManager : IContextUtility
|
|||||||
void UnregisterHandler(IPauseHandler handler);
|
void UnregisterHandler(IPauseHandler handler);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 暂停状态变化事件。
|
/// 暂停状态变化事件
|
||||||
/// 事件遵循标准 .NET 事件模式,事件源为触发通知的暂停管理器实例,
|
|
||||||
/// 事件数据由 <see cref="PauseStateChangedEventArgs"/> 提供。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
|
event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||||
}
|
}
|
||||||
@ -1,30 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@ -12,7 +12,6 @@ GFramework 框架的抽象层定义模块,包含所有核心组件的接口定
|
|||||||
- 事件系统接口 (IEvent, IEventBus)
|
- 事件系统接口 (IEvent, IEventBus)
|
||||||
- 依赖注入容器接口 (IIocContainer)
|
- 依赖注入容器接口 (IIocContainer)
|
||||||
- 可绑定属性接口 (IBindableProperty)
|
- 可绑定属性接口 (IBindableProperty)
|
||||||
- 状态管理接口 (IStore, IReducer, IStateSelector, IStoreBuilder)
|
|
||||||
- 日志系统接口 (ILogger)
|
- 日志系统接口 (ILogger)
|
||||||
|
|
||||||
## 设计原则
|
## 设计原则
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.Rule;
|
namespace GFramework.Core.Abstractions.Rule;
|
||||||
|
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
using GFramework.Core.Abstractions.Events;
|
|
||||||
|
|
||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 只读状态容器接口,用于暴露应用状态快照和订阅能力。
|
|
||||||
/// 该抽象适用于 Controller、Query、ViewModel 等只需要观察状态的调用方,
|
|
||||||
/// 使其无需依赖写入能力即可响应复杂状态树的变化。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
public interface IReadonlyStore<out TState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前状态快照。
|
|
||||||
/// Store 负责保证返回值与最近一次成功分发后的状态一致。
|
|
||||||
/// </summary>
|
|
||||||
TState State { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 订阅状态变化通知。
|
|
||||||
/// 仅当 Store 判断状态发生有效变化时,才会调用该监听器。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="listener">状态变化时的监听器,参数为新的状态快照。</param>
|
|
||||||
/// <returns>用于取消订阅的句柄。</returns>
|
|
||||||
IUnRegister Subscribe(Action<TState> listener);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 订阅状态变化通知,并立即以当前状态调用一次监听器。
|
|
||||||
/// 该方法适合在 UI 初始化或 ViewModel 首次绑定时建立同步视图。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="listener">状态变化时的监听器,参数为新的状态快照。</param>
|
|
||||||
/// <returns>用于取消订阅的句柄。</returns>
|
|
||||||
IUnRegister SubscribeWithInitValue(Action<TState> listener);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 取消订阅指定的状态监听器。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="listener">需要移除的监听器。</param>
|
|
||||||
void UnSubscribe(Action<TState> listener);
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 定义状态归约器接口。
|
|
||||||
/// Reducer 应保持纯函数风格:根据当前状态和 action 计算下一状态,
|
|
||||||
/// 不直接产生副作用,也不依赖外部可变环境。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
/// <typeparam name="TAction">当前 reducer 处理的 action 类型。</typeparam>
|
|
||||||
public interface IReducer<TState, in TAction>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 根据当前状态和 action 计算下一状态。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="currentState">当前状态快照。</param>
|
|
||||||
/// <param name="action">触发本次归约的 action。</param>
|
|
||||||
/// <returns>归约后的下一状态。</returns>
|
|
||||||
TState Reduce(TState currentState, TAction action);
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 定义状态选择器接口,用于从整棵状态树中投影出局部状态视图。
|
|
||||||
/// 该抽象适用于复用复杂选择逻辑,避免在 UI 或 Controller 中重复编写投影代码。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
|
||||||
/// <typeparam name="TSelected">投影后的局部状态类型。</typeparam>
|
|
||||||
public interface IStateSelector<in TState, out TSelected>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 从给定状态中选择目标片段。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">当前完整状态。</param>
|
|
||||||
/// <returns>投影后的局部状态。</returns>
|
|
||||||
TSelected Select(TState state);
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 可写状态容器接口,提供统一的状态分发入口。
|
|
||||||
/// 所有状态变更都应通过分发 action 触发,以保持单向数据流和可测试性。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
public interface IStore<out TState> : IReadonlyStore<TState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前是否可以撤销到更早的历史状态。
|
|
||||||
/// 当未启用历史缓冲区,或当前已经位于最早历史点时,返回 <see langword="false"/>。
|
|
||||||
/// </summary>
|
|
||||||
bool CanUndo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前是否可以重做到更晚的历史状态。
|
|
||||||
/// 当未启用历史缓冲区,或当前已经位于最新历史点时,返回 <see langword="false"/>。
|
|
||||||
/// </summary>
|
|
||||||
bool CanRedo { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 分发一个 action 以触发状态演进。
|
|
||||||
/// Store 会按注册顺序执行与该 action 类型匹配的 reducer,并在状态变化后通知订阅者。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TAction">action 的具体类型。</typeparam>
|
|
||||||
/// <param name="action">要分发的 action 实例。</param>
|
|
||||||
void Dispatch<TAction>(TAction action);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将多个状态操作合并到一个批处理中执行。
|
|
||||||
/// 批处理内部的每次分发仍会立即更新 Store 状态和历史,但订阅通知会延迟到最外层批处理结束后再统一触发一次。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="batchAction">批处理主体;调用方应在其中执行若干次 <see cref="Dispatch{TAction}(TAction)"/>、<see cref="Undo"/> 或 <see cref="Redo"/>。</param>
|
|
||||||
void RunInBatch(Action batchAction);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将当前状态回退到上一个历史点。
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="InvalidOperationException">当历史缓冲区未启用,或当前已经没有可撤销的历史点时抛出。</exception>
|
|
||||||
void Undo();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将当前状态前进到下一个历史点。
|
|
||||||
/// </summary>
|
|
||||||
/// <exception cref="InvalidOperationException">当历史缓冲区未启用,或当前已经没有可重做的历史点时抛出。</exception>
|
|
||||||
void Redo();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 跳转到指定索引的历史点。
|
|
||||||
/// 该能力适合调试面板或开发工具实现时间旅行查看。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="historyIndex">目标历史索引,从 0 开始。</param>
|
|
||||||
/// <exception cref="InvalidOperationException">当历史缓冲区未启用时抛出。</exception>
|
|
||||||
/// <exception cref="ArgumentOutOfRangeException">当 <paramref name="historyIndex"/> 超出当前历史范围时抛出。</exception>
|
|
||||||
void TimeTravelTo(int historyIndex);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 清空当前撤销/重做历史,并以当前状态作为新的历史锚点。
|
|
||||||
/// 该操作不会修改当前状态,也不会触发额外通知。
|
|
||||||
/// </summary>
|
|
||||||
void ClearHistory();
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 定义 Store 构建器接口,用于在创建 Store 之前完成 reducer、中间件和比较器配置。
|
|
||||||
/// 该抽象适用于模块化注册、依赖注入装配和测试工厂,避免调用方必须依赖具体 Store 类型进行配置。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
public interface IStoreBuilder<TState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 配置用于判断状态是否真正变化的比较器。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="comparer">状态比较器。</param>
|
|
||||||
/// <returns>当前构建器实例。</returns>
|
|
||||||
IStoreBuilder<TState> WithComparer(IEqualityComparer<TState> comparer);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 配置历史缓冲区容量。
|
|
||||||
/// 传入 0 表示禁用历史记录;大于 0 时会保留最近若干个状态快照,用于撤销、重做和时间旅行调试。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="historyCapacity">历史缓冲区容量。</param>
|
|
||||||
/// <returns>当前构建器实例。</returns>
|
|
||||||
IStoreBuilder<TState> WithHistoryCapacity(int historyCapacity);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 配置 reducer 的 action 匹配策略。
|
|
||||||
/// 默认使用 <see cref="StoreActionMatchingMode.ExactTypeOnly"/>,仅在需要复用基类或接口 action 层次时再启用多态匹配。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="actionMatchingMode">要使用的匹配策略。</param>
|
|
||||||
/// <returns>当前构建器实例。</returns>
|
|
||||||
IStoreBuilder<TState> WithActionMatching(StoreActionMatchingMode actionMatchingMode);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 添加一个强类型 reducer。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TAction">当前 reducer 处理的 action 类型。</typeparam>
|
|
||||||
/// <param name="reducer">要添加的 reducer。</param>
|
|
||||||
/// <returns>当前构建器实例。</returns>
|
|
||||||
IStoreBuilder<TState> AddReducer<TAction>(IReducer<TState, TAction> reducer);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 使用委托快速添加一个 reducer。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TAction">当前 reducer 处理的 action 类型。</typeparam>
|
|
||||||
/// <param name="reducer">执行归约的委托。</param>
|
|
||||||
/// <returns>当前构建器实例。</returns>
|
|
||||||
IStoreBuilder<TState> AddReducer<TAction>(Func<TState, TAction, TState> reducer);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 添加一个 Store 中间件。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="middleware">要添加的中间件。</param>
|
|
||||||
/// <returns>当前构建器实例。</returns>
|
|
||||||
IStoreBuilder<TState> UseMiddleware(IStoreMiddleware<TState> middleware);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 基于给定初始状态创建一个新的 Store。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="initialState">Store 的初始状态。</param>
|
|
||||||
/// <returns>已应用当前构建器配置的 Store 实例。</returns>
|
|
||||||
IStore<TState> Build(TState initialState);
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 暴露 Store 的诊断信息。
|
|
||||||
/// 该接口用于调试、监控和后续时间旅行能力的扩展,不参与状态写入流程。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
public interface IStoreDiagnostics<TState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前已注册的订阅者数量。
|
|
||||||
/// </summary>
|
|
||||||
int SubscriberCount { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取最近一次分发的 action 类型。
|
|
||||||
/// 即使该次分发未引起状态变化,该值也会更新。
|
|
||||||
/// </summary>
|
|
||||||
Type? LastActionType { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取最近一次真正改变状态的时间戳。
|
|
||||||
/// 若尚未发生状态变化,则返回 <see langword="null"/>。
|
|
||||||
/// </summary>
|
|
||||||
DateTimeOffset? LastStateChangedAt { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取最近一次分发记录。
|
|
||||||
/// </summary>
|
|
||||||
StoreDispatchRecord<TState>? LastDispatchRecord { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前 Store 使用的 action 匹配策略。
|
|
||||||
/// </summary>
|
|
||||||
StoreActionMatchingMode ActionMatchingMode { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取历史缓冲区容量。
|
|
||||||
/// 返回 0 表示当前 Store 未启用历史记录能力。
|
|
||||||
/// </summary>
|
|
||||||
int HistoryCapacity { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前可见历史记录数量。
|
|
||||||
/// 当历史记录启用时,该值至少为 1,因为当前状态会作为历史锚点存在。
|
|
||||||
/// </summary>
|
|
||||||
int HistoryCount { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前状态在历史缓冲区中的索引。
|
|
||||||
/// 当未启用历史记录时返回 -1。
|
|
||||||
/// </summary>
|
|
||||||
int HistoryIndex { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前是否处于批处理阶段。
|
|
||||||
/// 该值为 <see langword="true"/> 时,状态变更通知会延迟到最外层批处理结束后再统一发送。
|
|
||||||
/// </summary>
|
|
||||||
bool IsBatching { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前历史快照列表的只读快照。
|
|
||||||
/// 该方法会返回一份独立快照,供调试工具渲染时间旅行面板,而不暴露 Store 的内部可变集合。
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>当前历史快照列表;若未启用历史记录或当前没有历史,则返回空数组。</returns>
|
|
||||||
IReadOnlyList<StoreHistoryEntry<TState>> GetHistoryEntriesSnapshot();
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 定义 Store 分发中间件接口。
|
|
||||||
/// 中间件用于在 action 分发前后插入日志、诊断、审计或拦截逻辑,
|
|
||||||
/// 同时保持核心 Store 实现专注于状态归约与订阅通知。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
public interface IStoreMiddleware<TState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 执行一次分发管线节点。
|
|
||||||
/// 实现通常应调用 <paramref name="next"/> 继续后续处理;若选择短路,
|
|
||||||
/// 需要自行保证上下文状态对调用方仍然是可解释的。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">当前分发上下文。</param>
|
|
||||||
/// <param name="next">继续执行后续中间件或 reducer 的委托。</param>
|
|
||||||
void Invoke(StoreDispatchContext<TState> context, Action next);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 定义 Store 在分发 action 时的 reducer 匹配策略。
|
|
||||||
/// 默认使用精确类型匹配,以保持执行结果和顺序的确定性;仅在确有需要时再启用多态匹配。
|
|
||||||
/// </summary>
|
|
||||||
public enum StoreActionMatchingMode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 仅匹配与 action 运行时类型完全相同的 reducer。
|
|
||||||
/// 该模式不会命中基类或接口注册,适合作为默认的稳定行为。
|
|
||||||
/// </summary>
|
|
||||||
ExactTypeOnly = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 在精确类型匹配之外,额外匹配可赋值的基类和接口 reducer。
|
|
||||||
/// Store 会保持确定性的执行顺序:精确类型优先,其次是最近的基类,最后是接口注册。
|
|
||||||
/// </summary>
|
|
||||||
IncludeAssignableTypes = 1
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 表示一次 Store 分发流程中的上下文数据。
|
|
||||||
/// 中间件和 Store 实现通过该对象共享当前 action、分发时间以及归约结果。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
public sealed class StoreDispatchContext<TState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化一个新的分发上下文。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="action">当前分发的 action。</param>
|
|
||||||
/// <param name="previousState">分发前的状态快照。</param>
|
|
||||||
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
|
|
||||||
public StoreDispatchContext(object action, TState previousState)
|
|
||||||
{
|
|
||||||
Action = action ?? throw new ArgumentNullException(nameof(action));
|
|
||||||
PreviousState = previousState;
|
|
||||||
NextState = previousState;
|
|
||||||
DispatchedAt = DateTimeOffset.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前分发的 action 实例。
|
|
||||||
/// </summary>
|
|
||||||
public object Action { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前分发的 action 运行时类型。
|
|
||||||
/// </summary>
|
|
||||||
public Type ActionType => Action.GetType();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取分发前的状态快照。
|
|
||||||
/// </summary>
|
|
||||||
public TState PreviousState { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置归约后的下一状态。
|
|
||||||
/// Store 会在 reducer 执行完成后使用该值更新内部状态。
|
|
||||||
/// </summary>
|
|
||||||
public TState NextState { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置本次分发是否导致状态发生变化。
|
|
||||||
/// 中间件可读取该值进行日志和诊断,但通常应由 Store 负责最终判定。
|
|
||||||
/// </summary>
|
|
||||||
public bool HasStateChanged { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取本次分发创建时的时间戳。
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeOffset DispatchedAt { get; }
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 记录最近一次 Store 分发的结果。
|
|
||||||
/// 该结构为调试和诊断提供稳定的只读视图,避免调用方直接依赖 Store 的内部状态。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
public sealed class StoreDispatchRecord<TState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化一条分发记录。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="action">本次分发的 action。</param>
|
|
||||||
/// <param name="previousState">分发前状态。</param>
|
|
||||||
/// <param name="nextState">分发后状态。</param>
|
|
||||||
/// <param name="hasStateChanged">是否发生了有效状态变化。</param>
|
|
||||||
/// <param name="dispatchedAt">分发时间。</param>
|
|
||||||
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
|
|
||||||
public StoreDispatchRecord(
|
|
||||||
object action,
|
|
||||||
TState previousState,
|
|
||||||
TState nextState,
|
|
||||||
bool hasStateChanged,
|
|
||||||
DateTimeOffset dispatchedAt)
|
|
||||||
{
|
|
||||||
Action = action ?? throw new ArgumentNullException(nameof(action));
|
|
||||||
PreviousState = previousState;
|
|
||||||
NextState = nextState;
|
|
||||||
HasStateChanged = hasStateChanged;
|
|
||||||
DispatchedAt = dispatchedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取本次分发的 action 实例。
|
|
||||||
/// </summary>
|
|
||||||
public object Action { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取本次分发的 action 运行时类型。
|
|
||||||
/// </summary>
|
|
||||||
public Type ActionType => Action.GetType();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取分发前状态。
|
|
||||||
/// </summary>
|
|
||||||
public TState PreviousState { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取分发后状态。
|
|
||||||
/// </summary>
|
|
||||||
public TState NextState { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取本次分发是否产生了有效状态变化。
|
|
||||||
/// </summary>
|
|
||||||
public bool HasStateChanged { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取分发时间。
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeOffset DispatchedAt { get; }
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 表示一条 Store 历史快照记录。
|
|
||||||
/// 该记录用于撤销/重做和调试面板查看历史状态,不会暴露 Store 的内部可变结构。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
public sealed class StoreHistoryEntry<TState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化一条历史记录。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">该历史点对应的状态快照。</param>
|
|
||||||
/// <param name="recordedAt">该历史点被记录的时间。</param>
|
|
||||||
/// <param name="action">触发该状态的 action;若为初始状态或已清空历史后的锚点,则为 <see langword="null"/>。</param>
|
|
||||||
public StoreHistoryEntry(TState state, DateTimeOffset recordedAt, object? action = null)
|
|
||||||
{
|
|
||||||
State = state;
|
|
||||||
RecordedAt = recordedAt;
|
|
||||||
Action = action;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取该历史点对应的状态快照。
|
|
||||||
/// </summary>
|
|
||||||
public TState State { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取该历史点被记录的时间。
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeOffset RecordedAt { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取触发该历史点的 action 实例。
|
|
||||||
/// 对于初始状态或调用 <c>ClearHistory()</c> 后的新锚点,该值为 <see langword="null"/>。
|
|
||||||
/// </summary>
|
|
||||||
public object? Action { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取触发该历史点的 action 运行时类型。
|
|
||||||
/// 若该历史点没有关联 action,则返回 <see langword="null"/>。
|
|
||||||
/// </summary>
|
|
||||||
public Type? ActionType => Action?.GetType();
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Lifecycle;
|
using GFramework.Core.Abstractions.Lifecycle;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
namespace GFramework.Core.Abstractions.Utility.Numeric;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 数值显示风格。
|
|
||||||
/// </summary>
|
|
||||||
public enum NumericDisplayStyle
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 紧凑缩写风格,例如 1.2K / 3.4M。
|
|
||||||
/// </summary>
|
|
||||||
Compact = 0
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
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);
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Abstractions.Properties;
|
using GFramework.Core.Abstractions.Properties;
|
||||||
using GFramework.Core.Architectures;
|
using GFramework.Core.Architectures;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Command;
|
using GFramework.Core.Abstractions.Command;
|
||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
using GFramework.Core.Abstractions.Environment;
|
using GFramework.Core.Abstractions.Environment;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Command;
|
using GFramework.Core.Abstractions.Command;
|
||||||
using GFramework.Core.Abstractions.Environment;
|
using GFramework.Core.Abstractions.Environment;
|
||||||
using GFramework.Core.Abstractions.Events;
|
using GFramework.Core.Abstractions.Events;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Architectures;
|
using GFramework.Core.Architectures;
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Architectures;
|
namespace GFramework.Core.Tests.Architectures;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Command;
|
using GFramework.Core.Abstractions.Command;
|
||||||
using GFramework.Core.Abstractions.Environment;
|
using GFramework.Core.Abstractions.Environment;
|
||||||
using GFramework.Core.Abstractions.Events;
|
using GFramework.Core.Abstractions.Events;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
using GFramework.Core.Abstractions.Lifecycle;
|
using GFramework.Core.Abstractions.Lifecycle;
|
||||||
using GFramework.Core.Abstractions.Model;
|
using GFramework.Core.Abstractions.Model;
|
||||||
|
|||||||
@ -41,8 +41,16 @@ public abstract class TestArchitectureBase : Architecture
|
|||||||
{
|
{
|
||||||
InitCalled = true;
|
InitCalled = true;
|
||||||
_postRegistrationHook?.Invoke(this);
|
_postRegistrationHook?.Invoke(this);
|
||||||
|
}
|
||||||
|
|
||||||
// 订阅阶段变更事件以记录历史
|
/// <summary>
|
||||||
PhaseChanged += phase => PhaseHistory.Add(phase);
|
/// 进入指定架构阶段时的处理方法,记录阶段历史
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="next">要进入的下一个架构阶段</param>
|
||||||
|
protected override void EnterPhase(ArchitecturePhase next)
|
||||||
|
{
|
||||||
|
base.EnterPhase(next);
|
||||||
|
// 记录进入的架构阶段到历史列表中
|
||||||
|
PhaseHistory.Add(next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Command;
|
using GFramework.Core.Abstractions.Command;
|
||||||
using GFramework.Core.Abstractions.Coroutine;
|
using GFramework.Core.Abstractions.Coroutine;
|
||||||
using GFramework.Core.Abstractions.Events;
|
using GFramework.Core.Abstractions.Events;
|
||||||
@ -6,6 +6,7 @@ using GFramework.Core.Abstractions.Rule;
|
|||||||
using GFramework.Core.Coroutine.Extensions;
|
using GFramework.Core.Coroutine.Extensions;
|
||||||
using GFramework.Core.Coroutine.Instructions;
|
using GFramework.Core.Coroutine.Instructions;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Coroutine;
|
namespace GFramework.Core.Tests.Coroutine;
|
||||||
|
|
||||||
|
|||||||
@ -11,12 +11,13 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Coroutine;
|
using GFramework.Core.Abstractions.Coroutine;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
using GFramework.Core.Coroutine.Extensions;
|
using GFramework.Core.Coroutine.Extensions;
|
||||||
using Mediator;
|
using Mediator;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Coroutine;
|
namespace GFramework.Core.Tests.Coroutine;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Coroutine;
|
using GFramework.Core.Abstractions.Coroutine;
|
||||||
using GFramework.Core.Abstractions.Query;
|
using GFramework.Core.Abstractions.Query;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
using GFramework.Core.Coroutine.Extensions;
|
using GFramework.Core.Coroutine.Extensions;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Coroutine;
|
namespace GFramework.Core.Tests.Coroutine;
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TestTargetFrameworks Condition="'$(TestTargetFrameworks)' == ''">net10.0</TestTargetFrameworks>
|
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
|
||||||
<TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks>
|
|
||||||
<ImplicitUsings>disable</ImplicitUsings>
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<IsTestProject>true</IsTestProject>
|
<IsTestProject>true</IsTestProject>
|
||||||
<WarningLevel>0</WarningLevel>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Mediator.Abstractions" Version="3.0.1"/>
|
<PackageReference Include="Mediator.Abstractions" Version="3.0.1"/>
|
||||||
@ -22,8 +20,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Scriban" Version="6.6.0" />
|
|
||||||
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj"/>
|
|
||||||
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
||||||
<ProjectReference Include="..\GFramework.SourceGenerators.Abstractions\GFramework.SourceGenerators.Abstractions.csproj"/>
|
<ProjectReference Include="..\GFramework.SourceGenerators.Abstractions\GFramework.SourceGenerators.Abstractions.csproj"/>
|
||||||
<ProjectReference Include="..\GFramework.SourceGenerators.Common\GFramework.SourceGenerators.Common.csproj"/>
|
<ProjectReference Include="..\GFramework.SourceGenerators.Common\GFramework.SourceGenerators.Common.csproj"/>
|
||||||
|
|||||||
@ -19,8 +19,3 @@ global using System.Threading.Tasks;
|
|||||||
global using NUnit.Framework;
|
global using NUnit.Framework;
|
||||||
global using NUnit.Compatibility;
|
global using NUnit.Compatibility;
|
||||||
global using GFramework.Core.Systems;
|
global using GFramework.Core.Systems;
|
||||||
global using GFramework.Core.Abstractions.StateManagement;
|
|
||||||
global using GFramework.Core.Extensions;
|
|
||||||
global using GFramework.Core.Property;
|
|
||||||
global using GFramework.Core.StateManagement;
|
|
||||||
global using GFramework.Core.Abstractions.Property;
|
|
||||||
@ -1,190 +0,0 @@
|
|||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
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,5 +1,6 @@
|
|||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Logging.Appenders;
|
using GFramework.Core.Logging.Appenders;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Logging;
|
namespace GFramework.Core.Tests.Logging;
|
||||||
|
|
||||||
@ -151,12 +152,8 @@ public class AsyncLogAppenderTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void Append_WhenInnerAppenderThrows_ShouldNotCrash()
|
public void Append_WhenInnerAppenderThrows_ShouldNotCrash()
|
||||||
{
|
{
|
||||||
var reportedExceptions = new List<Exception>();
|
|
||||||
var innerAppender = new ThrowingAppender();
|
var innerAppender = new ThrowingAppender();
|
||||||
using var asyncAppender = new AsyncLogAppender(
|
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000);
|
||||||
innerAppender,
|
|
||||||
bufferSize: 1000,
|
|
||||||
processingErrorHandler: reportedExceptions.Add);
|
|
||||||
|
|
||||||
// 即使内部 Appender 抛出异常,也不应该影响调用线程
|
// 即使内部 Appender 抛出异常,也不应该影响调用线程
|
||||||
Assert.DoesNotThrow(() =>
|
Assert.DoesNotThrow(() =>
|
||||||
@ -168,56 +165,7 @@ public class AsyncLogAppenderTests
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
asyncAppender.Flush();
|
Thread.Sleep(100); // 等待后台处理
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助测试类
|
// 辅助测试类
|
||||||
@ -280,20 +228,4 @@ 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,6 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Architectures;
|
using GFramework.Core.Architectures;
|
||||||
using GFramework.Core.Command;
|
using GFramework.Core.Command;
|
||||||
using GFramework.Core.Ioc;
|
using GFramework.Core.Ioc;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Events;
|
using GFramework.Core.Abstractions.Events;
|
||||||
using GFramework.Core.Architectures;
|
using GFramework.Core.Architectures;
|
||||||
using GFramework.Core.Command;
|
using GFramework.Core.Command;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
using GFramework.Core.Abstractions.Model;
|
using GFramework.Core.Abstractions.Model;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using GFramework.Core.Abstractions.Pause;
|
using GFramework.Core.Abstractions.Pause;
|
||||||
using GFramework.Core.Pause;
|
using GFramework.Core.Pause;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.Pause;
|
namespace GFramework.Core.Tests.Pause;
|
||||||
|
|
||||||
@ -219,11 +220,11 @@ public class PauseStackManagerTests
|
|||||||
PauseGroup? eventGroup = null;
|
PauseGroup? eventGroup = null;
|
||||||
bool? eventIsPaused = null;
|
bool? eventIsPaused = null;
|
||||||
|
|
||||||
_manager.OnPauseStateChanged += (_, e) =>
|
_manager.OnPauseStateChanged += (group, isPaused) =>
|
||||||
{
|
{
|
||||||
eventTriggered = true;
|
eventTriggered = true;
|
||||||
eventGroup = e.Group;
|
eventGroup = group;
|
||||||
eventIsPaused = e.IsPaused;
|
eventIsPaused = isPaused;
|
||||||
};
|
};
|
||||||
|
|
||||||
_manager.Push("Test", PauseGroup.Gameplay);
|
_manager.Push("Test", PauseGroup.Gameplay);
|
||||||
@ -242,10 +243,10 @@ public class PauseStackManagerTests
|
|||||||
var token = _manager.Push("Test");
|
var token = _manager.Push("Test");
|
||||||
|
|
||||||
bool eventTriggered = false;
|
bool eventTriggered = false;
|
||||||
_manager.OnPauseStateChanged += (_, e) =>
|
_manager.OnPauseStateChanged += (group, isPaused) =>
|
||||||
{
|
{
|
||||||
eventTriggered = true;
|
eventTriggered = true;
|
||||||
Assert.That(e.IsPaused, Is.False);
|
Assert.That(isPaused, Is.False);
|
||||||
};
|
};
|
||||||
|
|
||||||
_manager.Pop(token);
|
_manager.Pop(token);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
using GFramework.Core.Abstractions.Model;
|
using GFramework.Core.Abstractions.Model;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Rule;
|
using GFramework.Core.Abstractions.Rule;
|
||||||
using GFramework.Core.Architectures;
|
using GFramework.Core.Architectures;
|
||||||
using GFramework.Core.Rule;
|
using GFramework.Core.Rule;
|
||||||
|
|||||||
@ -1,92 +0,0 @@
|
|||||||
using GFramework.Core.Events;
|
|
||||||
|
|
||||||
namespace GFramework.Core.Tests.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Store 到 EventBus 桥接扩展的单元测试。
|
|
||||||
/// 这些测试验证旧模块兼容桥接能够正确转发 dispatch 和状态变化事件,并支持运行时拆除。
|
|
||||||
/// </summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class StoreEventBusExtensionsTests
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 测试桥接会发布每次 dispatch 事件,并对批处理后的状态变化只发送一次最终状态事件。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void BridgeToEventBus_Should_Publish_Dispatches_And_Collapsed_State_Changes()
|
|
||||||
{
|
|
||||||
var eventBus = new EventBus();
|
|
||||||
var store = CreateStore();
|
|
||||||
var dispatchedEvents = new List<StoreDispatchedEvent<CounterState>>();
|
|
||||||
var stateChangedEvents = new List<StoreStateChangedEvent<CounterState>>();
|
|
||||||
|
|
||||||
eventBus.Register<StoreDispatchedEvent<CounterState>>(dispatchedEvents.Add);
|
|
||||||
eventBus.Register<StoreStateChangedEvent<CounterState>>(stateChangedEvents.Add);
|
|
||||||
|
|
||||||
store.BridgeToEventBus(eventBus);
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
store.RunInBatch(() =>
|
|
||||||
{
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.That(dispatchedEvents.Count, Is.EqualTo(3));
|
|
||||||
Assert.That(dispatchedEvents[0].DispatchRecord.NextState.Count, Is.EqualTo(1));
|
|
||||||
Assert.That(dispatchedEvents[2].DispatchRecord.NextState.Count, Is.EqualTo(3));
|
|
||||||
|
|
||||||
Assert.That(stateChangedEvents.Count, Is.EqualTo(2));
|
|
||||||
Assert.That(stateChangedEvents[0].State.Count, Is.EqualTo(1));
|
|
||||||
Assert.That(stateChangedEvents[1].State.Count, Is.EqualTo(3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试桥接句柄注销后不会再继续向 EventBus 发送事件。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void BridgeToEventBus_UnRegister_Should_Stop_Future_Publications()
|
|
||||||
{
|
|
||||||
var eventBus = new EventBus();
|
|
||||||
var store = CreateStore();
|
|
||||||
var dispatchedEvents = new List<StoreDispatchedEvent<CounterState>>();
|
|
||||||
var stateChangedEvents = new List<StoreStateChangedEvent<CounterState>>();
|
|
||||||
|
|
||||||
eventBus.Register<StoreDispatchedEvent<CounterState>>(dispatchedEvents.Add);
|
|
||||||
eventBus.Register<StoreStateChangedEvent<CounterState>>(stateChangedEvents.Add);
|
|
||||||
|
|
||||||
var bridge = store.BridgeToEventBus(eventBus);
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
bridge.UnRegister();
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
|
|
||||||
Assert.That(dispatchedEvents.Count, Is.EqualTo(1));
|
|
||||||
Assert.That(stateChangedEvents.Count, Is.EqualTo(1));
|
|
||||||
Assert.That(stateChangedEvents[0].State.Count, Is.EqualTo(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建一个带基础 reducer 的测试 Store。
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>测试用 Store 实例。</returns>
|
|
||||||
private static Store<CounterState> CreateStore()
|
|
||||||
{
|
|
||||||
var store = new Store<CounterState>(new CounterState(0, "Player"));
|
|
||||||
store.RegisterReducer<IncrementAction>((state, action) => state with { Count = state.Count + action.Amount });
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用于桥接测试的状态类型。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Count">当前计数值。</param>
|
|
||||||
/// <param name="Name">当前名称。</param>
|
|
||||||
private sealed record CounterState(int Count, string Name);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用于桥接测试的计数 action。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Amount">要增加的数量。</param>
|
|
||||||
private sealed record IncrementAction(int Amount);
|
|
||||||
}
|
|
||||||
@ -1,879 +0,0 @@
|
|||||||
namespace GFramework.Core.Tests.StateManagement;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Store 状态管理能力的单元测试。
|
|
||||||
/// 这些测试覆盖集中式状态容器的核心职责:状态归约、订阅通知、选择器桥接和诊断行为。
|
|
||||||
/// </summary>
|
|
||||||
[TestFixture]
|
|
||||||
public class StoreTests
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 Store 在创建后能够暴露初始状态。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void State_Should_Return_Initial_State()
|
|
||||||
{
|
|
||||||
var store = CreateStore(new CounterState(1, "Player"));
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(1));
|
|
||||||
Assert.That(store.State.Name, Is.EqualTo("Player"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 Dispatch 能够执行 reducer 并向订阅者广播新状态。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Dispatch_Should_Update_State_And_Notify_Subscribers()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var receivedStates = new List<CounterState>();
|
|
||||||
|
|
||||||
store.Subscribe(receivedStates.Add);
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(2));
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(2));
|
|
||||||
Assert.That(receivedStates.Count, Is.EqualTo(1));
|
|
||||||
Assert.That(receivedStates[0].Count, Is.EqualTo(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试当 reducer 返回逻辑相等状态时不会触发通知。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Dispatch_Should_Not_Notify_When_State_Does_Not_Change()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var notifyCount = 0;
|
|
||||||
|
|
||||||
store.Subscribe(_ => notifyCount++);
|
|
||||||
|
|
||||||
store.Dispatch(new RenameAction("Player"));
|
|
||||||
|
|
||||||
Assert.That(store.State.Name, Is.EqualTo("Player"));
|
|
||||||
Assert.That(notifyCount, Is.EqualTo(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试同一 action 类型的多个 reducer 会按注册顺序执行。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Dispatch_Should_Run_Multiple_Reducers_In_Registration_Order()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
store.RegisterReducer<IncrementAction>((state, action) =>
|
|
||||||
state with { Count = state.Count + action.Amount * 10 });
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(11));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 SubscribeWithInitValue 会立即回放当前状态并继续接收后续变化。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void SubscribeWithInitValue_Should_Replay_Current_State_And_Future_Changes()
|
|
||||||
{
|
|
||||||
var store = CreateStore(new CounterState(5, "Player"));
|
|
||||||
var receivedCounts = new List<int>();
|
|
||||||
|
|
||||||
store.SubscribeWithInitValue(state => receivedCounts.Add(state.Count));
|
|
||||||
store.Dispatch(new IncrementAction(3));
|
|
||||||
|
|
||||||
Assert.That(receivedCounts, Is.EqualTo(new[] { 5, 8 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 Store 的 SubscribeWithInitValue 在初始化回放期间不会漏掉后续状态变化。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void SubscribeWithInitValue_Should_Not_Miss_Changes_During_Init_Callback()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var receivedCounts = new List<int>();
|
|
||||||
|
|
||||||
store.SubscribeWithInitValue(state =>
|
|
||||||
{
|
|
||||||
receivedCounts.Add(state.Count);
|
|
||||||
if (receivedCounts.Count == 1)
|
|
||||||
{
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.That(receivedCounts, Is.EqualTo(new[] { 0, 1 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试注销订阅后不会再收到后续通知。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void UnRegister_Handle_Should_Stop_Future_Notifications()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var notifyCount = 0;
|
|
||||||
|
|
||||||
var unRegister = store.Subscribe(_ => notifyCount++);
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
unRegister.UnRegister();
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
|
|
||||||
Assert.That(notifyCount, Is.EqualTo(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试选择器仅在所选状态片段变化时触发通知。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Select_Should_Only_Notify_When_Selected_Slice_Changes()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var selectedCounts = new List<int>();
|
|
||||||
var selection = store.Select(state => state.Count);
|
|
||||||
|
|
||||||
selection.Register(selectedCounts.Add);
|
|
||||||
|
|
||||||
store.Dispatch(new RenameAction("Renamed"));
|
|
||||||
store.Dispatch(new IncrementAction(2));
|
|
||||||
|
|
||||||
Assert.That(selectedCounts, Is.EqualTo(new[] { 2 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试选择器支持自定义比较器,从而抑制无意义的局部状态通知。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Select_Should_Respect_Custom_Selected_Value_Comparer()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var selectedCounts = new List<int>();
|
|
||||||
var selection = store.Select(
|
|
||||||
state => state.Count,
|
|
||||||
new TensBucketEqualityComparer());
|
|
||||||
|
|
||||||
selection.Register(selectedCounts.Add);
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(5));
|
|
||||||
store.Dispatch(new IncrementAction(6));
|
|
||||||
|
|
||||||
Assert.That(selectedCounts, Is.EqualTo(new[] { 11 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 StoreSelection 的 RegisterWithInitValue 在初始化回放期间不会漏掉后续局部状态变化。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Selection_RegisterWithInitValue_Should_Not_Miss_Changes_During_Init_Callback()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var selection = store.Select(state => state.Count);
|
|
||||||
var receivedCounts = new List<int>();
|
|
||||||
|
|
||||||
selection.RegisterWithInitValue(value =>
|
|
||||||
{
|
|
||||||
receivedCounts.Add(value);
|
|
||||||
if (receivedCounts.Count == 1)
|
|
||||||
{
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.That(receivedCounts, Is.EqualTo(new[] { 0, 1 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 ToBindableProperty 可桥接到现有 BindableProperty 风格用法,并与旧属性系统共存。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void ToBindableProperty_Should_Work_With_Existing_BindableProperty_Pattern()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var mirror = new BindableProperty<int>(0);
|
|
||||||
IReadonlyBindableProperty<int> bindableProperty = store.ToBindableProperty(state => state.Count);
|
|
||||||
|
|
||||||
bindableProperty.Register(value => mirror.Value = value);
|
|
||||||
store.Dispatch(new IncrementAction(3));
|
|
||||||
|
|
||||||
Assert.That(mirror.Value, Is.EqualTo(3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 IStateSelector 接口重载能够复用显式选择逻辑。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Select_With_IStateSelector_Should_Project_Selected_Value()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var selection = store.Select(new CounterNameSelector());
|
|
||||||
|
|
||||||
Assert.That(selection.Value, Is.EqualTo("Player"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 Store 在中间件内部发生同一实例的嵌套分发时会抛出异常。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Dispatch_Should_Throw_When_Nested_Dispatch_Happens_On_Same_Store()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
store.UseMiddleware(new NestedDispatchMiddleware(store));
|
|
||||||
|
|
||||||
Assert.That(
|
|
||||||
() => store.Dispatch(new IncrementAction(1)),
|
|
||||||
Throws.InvalidOperationException.With.Message.Contain("Nested dispatch"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试中间件链执行顺序和 Store 诊断信息更新。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Dispatch_Should_Run_Middlewares_In_Order_And_Update_Diagnostics()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var logs = new List<string>();
|
|
||||||
|
|
||||||
store.UseMiddleware(new RecordingMiddleware(logs, "first"));
|
|
||||||
store.UseMiddleware(new RecordingMiddleware(logs, "second"));
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(2));
|
|
||||||
|
|
||||||
Assert.That(logs, Is.EqualTo(new[]
|
|
||||||
{
|
|
||||||
"first:before",
|
|
||||||
"second:before",
|
|
||||||
"second:after",
|
|
||||||
"first:after"
|
|
||||||
}));
|
|
||||||
|
|
||||||
Assert.That(store.LastActionType, Is.EqualTo(typeof(IncrementAction)));
|
|
||||||
Assert.That(store.LastStateChangedAt, Is.Not.Null);
|
|
||||||
Assert.That(store.LastDispatchRecord, Is.Not.Null);
|
|
||||||
Assert.That(store.LastDispatchRecord!.HasStateChanged, Is.True);
|
|
||||||
Assert.That(store.LastDispatchRecord.NextState.Count, Is.EqualTo(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 reducer 句柄注销后,后续同类型 action 不会再命中该 reducer。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void RegisterReducerHandle_UnRegister_Should_Stop_Future_Reductions()
|
|
||||||
{
|
|
||||||
var store = new Store<CounterState>(new CounterState(0, "Player"));
|
|
||||||
var reducerHandle = store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
|
||||||
state with { Count = state.Count + action.Amount });
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(2));
|
|
||||||
reducerHandle.UnRegister();
|
|
||||||
store.Dispatch(new IncrementAction(2));
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 middleware 句柄注销后,后续 dispatch 不会再经过该中间件。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void RegisterMiddleware_UnRegister_Should_Stop_Future_Pipeline_Execution()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var logs = new List<string>();
|
|
||||||
var middlewareHandle = store.RegisterMiddleware(new RecordingMiddleware(logs, "dynamic"));
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
middlewareHandle.UnRegister();
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(2));
|
|
||||||
Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试移除同一 action 类型中的某个 reducer 后,其余 reducer 仍保持原有注册顺序。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void RegisterReducerHandle_UnRegister_Should_Preserve_Remaining_Order()
|
|
||||||
{
|
|
||||||
var executionOrder = new List<string>();
|
|
||||||
var store = new Store<CounterState>(new CounterState(0, "Player"));
|
|
||||||
|
|
||||||
store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
|
||||||
{
|
|
||||||
executionOrder.Add("first");
|
|
||||||
return state with { Count = state.Count + action.Amount };
|
|
||||||
});
|
|
||||||
|
|
||||||
var middleReducer = store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
|
||||||
{
|
|
||||||
executionOrder.Add("middle");
|
|
||||||
return state with { Count = state.Count + action.Amount * 10 };
|
|
||||||
});
|
|
||||||
|
|
||||||
store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
|
||||||
{
|
|
||||||
executionOrder.Add("last");
|
|
||||||
return state with { Count = state.Count + action.Amount * 100 };
|
|
||||||
});
|
|
||||||
|
|
||||||
middleReducer.UnRegister();
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
|
|
||||||
Assert.That(executionOrder, Is.EqualTo(new[] { "first", "last" }));
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(101));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试注册句柄的注销操作是幂等的,多次调用不会抛异常或影响其他注册项。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void RegisterHandles_UnRegister_Should_Be_Idempotent()
|
|
||||||
{
|
|
||||||
var logs = new List<string>();
|
|
||||||
var store = new Store<CounterState>(new CounterState(0, "Player"));
|
|
||||||
var reducerHandle = store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
|
||||||
state with { Count = state.Count + action.Amount });
|
|
||||||
var middlewareHandle = store.RegisterMiddleware(new RecordingMiddleware(logs, "dynamic"));
|
|
||||||
|
|
||||||
Assert.That(() =>
|
|
||||||
{
|
|
||||||
reducerHandle.UnRegister();
|
|
||||||
reducerHandle.UnRegister();
|
|
||||||
middlewareHandle.UnRegister();
|
|
||||||
middlewareHandle.UnRegister();
|
|
||||||
}, Throws.Nothing);
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(0));
|
|
||||||
Assert.That(logs, Is.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 dispatch 进行中注销 reducer 和 middleware 时,
|
|
||||||
/// 当前 dispatch 仍使用开始时的快照,而后续 dispatch 会看到注销结果。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void UnRegister_During_Dispatch_Should_Affect_Next_Dispatch_But_Not_Current_One()
|
|
||||||
{
|
|
||||||
using var entered = new ManualResetEventSlim(false);
|
|
||||||
using var release = new ManualResetEventSlim(false);
|
|
||||||
|
|
||||||
var logs = new List<string>();
|
|
||||||
var store = new Store<CounterState>(new CounterState(0, "Player"));
|
|
||||||
var reducerHandle = store.RegisterReducerHandle<IncrementAction>((state, action) =>
|
|
||||||
state with { Count = state.Count + action.Amount });
|
|
||||||
var blockingHandle = store.RegisterMiddleware(new BlockingMiddleware(entered, release));
|
|
||||||
var recordingHandle = store.RegisterMiddleware(new RecordingMiddleware(logs, "dynamic"));
|
|
||||||
|
|
||||||
var dispatchTask = Task.Run(() => store.Dispatch(new IncrementAction(1)));
|
|
||||||
|
|
||||||
Assert.That(entered.Wait(TimeSpan.FromSeconds(2)), Is.True, "middleware 未按预期进入阻塞阶段");
|
|
||||||
|
|
||||||
reducerHandle.UnRegister();
|
|
||||||
blockingHandle.UnRegister();
|
|
||||||
recordingHandle.UnRegister();
|
|
||||||
release.Set();
|
|
||||||
|
|
||||||
Assert.That(dispatchTask.Wait(TimeSpan.FromSeconds(2)), Is.True, "dispatch 未在释放 middleware 后完成");
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(1), "当前 dispatch 应继续使用启动时抓取的 reducer 快照");
|
|
||||||
Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" }));
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(1), "后续 dispatch 应看到 reducer 已被注销");
|
|
||||||
Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Dispatch_Without_Matching_Reducer_Should_Update_Record_Without_Changing_State()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
|
|
||||||
store.Dispatch(new NoopAction());
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(0));
|
|
||||||
Assert.That(store.LastActionType, Is.EqualTo(typeof(NoopAction)));
|
|
||||||
Assert.That(store.LastDispatchRecord, Is.Not.Null);
|
|
||||||
Assert.That(store.LastDispatchRecord!.HasStateChanged, Is.False);
|
|
||||||
Assert.That(store.LastStateChangedAt, Is.Null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 Store 能够复用同一个缓存选择视图实例。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void GetOrCreateSelection_Should_Return_Cached_Instance_For_Same_Key()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
|
|
||||||
var first = store.GetOrCreateSelection("count", state => state.Count);
|
|
||||||
var second = store.GetOrCreateSelection("count", state => state.Count);
|
|
||||||
|
|
||||||
Assert.That(second, Is.SameAs(first));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 StoreBuilder 能够应用 reducer、中间件和状态比较器配置。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void StoreBuilder_Should_Apply_Configured_Reducers_Middlewares_And_Comparer()
|
|
||||||
{
|
|
||||||
var logs = new List<string>();
|
|
||||||
var store = (Store<CounterState>)Store<CounterState>
|
|
||||||
.CreateBuilder()
|
|
||||||
.WithComparer(new CounterStateNameInsensitiveComparer())
|
|
||||||
.AddReducer<IncrementAction>((state, action) => state with { Count = state.Count + action.Amount })
|
|
||||||
.AddReducer<RenameAction>((state, action) => state with { Name = action.Name })
|
|
||||||
.UseMiddleware(new RecordingMiddleware(logs, "builder"))
|
|
||||||
.Build(new CounterState(0, "Player"));
|
|
||||||
|
|
||||||
var notifyCount = 0;
|
|
||||||
store.Subscribe(_ => notifyCount++);
|
|
||||||
|
|
||||||
store.Dispatch(new RenameAction("player"));
|
|
||||||
store.Dispatch(new IncrementAction(2));
|
|
||||||
|
|
||||||
Assert.That(notifyCount, Is.EqualTo(1));
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(2));
|
|
||||||
Assert.That(logs, Is.EqualTo(new[] { "builder:before", "builder:after", "builder:before", "builder:after" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试批处理会折叠多次状态变化通知,只在最外层结束时发布最终状态。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void RunInBatch_Should_Collapse_Notifications_To_Final_State()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var receivedCounts = new List<int>();
|
|
||||||
|
|
||||||
store.Subscribe(state => receivedCounts.Add(state.Count));
|
|
||||||
|
|
||||||
store.RunInBatch(() =>
|
|
||||||
{
|
|
||||||
Assert.That(store.IsBatching, Is.True);
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
store.Dispatch(new IncrementAction(2));
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.That(store.IsBatching, Is.False);
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(3));
|
|
||||||
Assert.That(receivedCounts, Is.EqualTo(new[] { 3 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试嵌套批处理只会在最外层结束时发出一次通知。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void RunInBatch_Should_Support_Nested_Batches()
|
|
||||||
{
|
|
||||||
var store = CreateStore();
|
|
||||||
var receivedCounts = new List<int>();
|
|
||||||
|
|
||||||
store.Subscribe(state => receivedCounts.Add(state.Count));
|
|
||||||
|
|
||||||
store.RunInBatch(() =>
|
|
||||||
{
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
|
|
||||||
store.RunInBatch(() =>
|
|
||||||
{
|
|
||||||
Assert.That(store.IsBatching, Is.True);
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
});
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(3));
|
|
||||||
Assert.That(receivedCounts, Is.EqualTo(new[] { 3 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试启用历史记录后支持撤销、重做、时间旅行和 redo 分支裁剪。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void History_Should_Support_Undo_Redo_Time_Travel_And_Branch_Reset()
|
|
||||||
{
|
|
||||||
var store = new Store<CounterState>(new CounterState(0, "Player"), historyCapacity: 8);
|
|
||||||
store.RegisterReducer<IncrementAction>((state, action) => state with { Count = state.Count + action.Amount });
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
|
|
||||||
Assert.That(store.HistoryCount, Is.EqualTo(4));
|
|
||||||
Assert.That(store.HistoryIndex, Is.EqualTo(3));
|
|
||||||
Assert.That(store.CanUndo, Is.True);
|
|
||||||
Assert.That(store.CanRedo, Is.False);
|
|
||||||
|
|
||||||
store.Undo();
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(2));
|
|
||||||
Assert.That(store.HistoryIndex, Is.EqualTo(2));
|
|
||||||
Assert.That(store.CanRedo, Is.True);
|
|
||||||
|
|
||||||
store.Undo();
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(1));
|
|
||||||
Assert.That(store.HistoryIndex, Is.EqualTo(1));
|
|
||||||
|
|
||||||
store.Redo();
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(2));
|
|
||||||
Assert.That(store.HistoryIndex, Is.EqualTo(2));
|
|
||||||
|
|
||||||
store.TimeTravelTo(0);
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(0));
|
|
||||||
Assert.That(store.HistoryIndex, Is.EqualTo(0));
|
|
||||||
|
|
||||||
store.TimeTravelTo(2);
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(2));
|
|
||||||
Assert.That(store.HistoryIndex, Is.EqualTo(2));
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(10));
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(12));
|
|
||||||
Assert.That(store.CanRedo, Is.False, "新 dispatch 应清除 redo 分支");
|
|
||||||
Assert.That(store.GetHistoryEntriesSnapshot().Select(entry => entry.State.Count),
|
|
||||||
Is.EqualTo(new[] { 0, 1, 2, 12 }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 ClearHistory 会以当前状态重置历史锚点,而不会修改当前状态。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void ClearHistory_Should_Reset_To_Current_State_Anchor()
|
|
||||||
{
|
|
||||||
var store = new Store<CounterState>(new CounterState(0, "Player"), historyCapacity: 4);
|
|
||||||
store.RegisterReducer<IncrementAction>((state, action) => state with { Count = state.Count + action.Amount });
|
|
||||||
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
store.ClearHistory();
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(2));
|
|
||||||
Assert.That(store.HistoryCount, Is.EqualTo(1));
|
|
||||||
Assert.That(store.HistoryIndex, Is.EqualTo(0));
|
|
||||||
Assert.That(store.CanUndo, Is.False);
|
|
||||||
Assert.That(store.GetHistoryEntriesSnapshot()[0].State.Count, Is.EqualTo(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试默认 action 匹配策略仍然只命中精确类型 reducer。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Dispatch_Should_Remain_Exact_Type_Only_By_Default()
|
|
||||||
{
|
|
||||||
var store = new Store<CounterState>(new CounterState(0, "Player"));
|
|
||||||
store.RegisterReducer<IncrementActionBase>((state, action) =>
|
|
||||||
state with { Count = state.Count + action.Amount * 10 });
|
|
||||||
store.RegisterReducer<IIncrementActionMarker>((state, action) =>
|
|
||||||
state with { Count = state.Count + action.Amount * 100 });
|
|
||||||
|
|
||||||
store.Dispatch(new DerivedIncrementAction(1));
|
|
||||||
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(0));
|
|
||||||
Assert.That(store.ActionMatchingMode, Is.EqualTo(StoreActionMatchingMode.ExactTypeOnly));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试启用多态匹配后,Store 会按“精确类型 -> 基类 -> 接口”的稳定顺序执行 reducer。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Dispatch_Should_Use_Polymorphic_Action_Matching_In_Deterministic_Order()
|
|
||||||
{
|
|
||||||
var executionOrder = new List<string>();
|
|
||||||
var store = new Store<CounterState>(
|
|
||||||
new CounterState(0, "Player"),
|
|
||||||
actionMatchingMode: StoreActionMatchingMode.IncludeAssignableTypes);
|
|
||||||
|
|
||||||
store.RegisterReducer<IncrementActionBase>((state, action) =>
|
|
||||||
{
|
|
||||||
executionOrder.Add("base");
|
|
||||||
return state with { Count = state.Count + action.Amount * 10 };
|
|
||||||
});
|
|
||||||
|
|
||||||
store.RegisterReducer<IIncrementActionMarker>((state, action) =>
|
|
||||||
{
|
|
||||||
executionOrder.Add("interface");
|
|
||||||
return state with { Count = state.Count + action.Amount * 100 };
|
|
||||||
});
|
|
||||||
|
|
||||||
store.RegisterReducer<DerivedIncrementAction>((state, action) =>
|
|
||||||
{
|
|
||||||
executionOrder.Add("exact");
|
|
||||||
return state with { Count = state.Count + action.Amount };
|
|
||||||
});
|
|
||||||
|
|
||||||
store.Dispatch(new DerivedIncrementAction(1));
|
|
||||||
|
|
||||||
Assert.That(executionOrder, Is.EqualTo(new[] { "exact", "base", "interface" }));
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(111));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试 StoreBuilder 能够应用历史容量和 action 匹配策略配置。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void StoreBuilder_Should_Apply_History_And_Action_Matching_Configuration()
|
|
||||||
{
|
|
||||||
var store = (Store<CounterState>)Store<CounterState>
|
|
||||||
.CreateBuilder()
|
|
||||||
.WithHistoryCapacity(6)
|
|
||||||
.WithActionMatching(StoreActionMatchingMode.IncludeAssignableTypes)
|
|
||||||
.AddReducer<IncrementActionBase>((state, action) => state with { Count = state.Count + action.Amount })
|
|
||||||
.Build(new CounterState(0, "Player"));
|
|
||||||
|
|
||||||
store.Dispatch(new DerivedIncrementAction(2));
|
|
||||||
|
|
||||||
Assert.That(store.ActionMatchingMode, Is.EqualTo(StoreActionMatchingMode.IncludeAssignableTypes));
|
|
||||||
Assert.That(store.HistoryCapacity, Is.EqualTo(6));
|
|
||||||
Assert.That(store.HistoryCount, Is.EqualTo(2));
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试长时间运行的 middleware 不会长时间占用状态锁,
|
|
||||||
/// 使读取状态和新增订阅仍能在 dispatch 进行期间完成。
|
|
||||||
/// </summary>
|
|
||||||
[Test]
|
|
||||||
public void Dispatch_Should_Not_Block_State_Read_Or_Subscribe_While_Middleware_Is_Running()
|
|
||||||
{
|
|
||||||
using var entered = new ManualResetEventSlim(false);
|
|
||||||
using var release = new ManualResetEventSlim(false);
|
|
||||||
|
|
||||||
var store = CreateStore();
|
|
||||||
store.UseMiddleware(new BlockingMiddleware(entered, release));
|
|
||||||
|
|
||||||
var dispatchTask = Task.Run(() => store.Dispatch(new IncrementAction(1)));
|
|
||||||
|
|
||||||
Assert.That(entered.Wait(TimeSpan.FromSeconds(2)), Is.True, "middleware 未按预期进入阻塞阶段");
|
|
||||||
|
|
||||||
var stateReadTask = Task.Run(() => store.State.Count);
|
|
||||||
Assert.That(stateReadTask.Wait(TimeSpan.FromMilliseconds(200)), Is.True, "State 读取被 dispatch 长时间阻塞");
|
|
||||||
Assert.That(stateReadTask.Result, Is.EqualTo(0), "middleware 执行期间应仍能读取到提交前的状态快照");
|
|
||||||
|
|
||||||
var subscribeTask = Task.Run(() =>
|
|
||||||
{
|
|
||||||
var unRegister = store.Subscribe(_ => { });
|
|
||||||
unRegister.UnRegister();
|
|
||||||
});
|
|
||||||
Assert.That(subscribeTask.Wait(TimeSpan.FromMilliseconds(200)), Is.True, "Subscribe 被 dispatch 长时间阻塞");
|
|
||||||
|
|
||||||
release.Set();
|
|
||||||
|
|
||||||
Assert.That(dispatchTask.Wait(TimeSpan.FromSeconds(2)), Is.True, "dispatch 未在释放 middleware 后完成");
|
|
||||||
Assert.That(store.State.Count, Is.EqualTo(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建一个带有基础 reducer 的测试 Store。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="initialState">可选初始状态。</param>
|
|
||||||
/// <returns>已配置基础 reducer 的 Store 实例。</returns>
|
|
||||||
private static Store<CounterState> CreateStore(CounterState? initialState = null)
|
|
||||||
{
|
|
||||||
var store = new Store<CounterState>(initialState ?? new CounterState(0, "Player"));
|
|
||||||
store.RegisterReducer<IncrementAction>((state, action) => state with { Count = state.Count + action.Amount });
|
|
||||||
store.RegisterReducer<RenameAction>((state, action) => state with { Name = action.Name });
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用于测试的计数器状态。
|
|
||||||
/// 使用 record 保持逻辑不可变语义,便于 Store 基于状态快照进行比较和断言。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Count">当前计数值。</param>
|
|
||||||
/// <param name="Name">当前名称。</param>
|
|
||||||
private sealed record CounterState(int Count, string Name);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 表示增加计数的 action。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Amount">要增加的数量。</param>
|
|
||||||
private sealed record IncrementAction(int Amount);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 表示修改名称的 action。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Name">新的名称。</param>
|
|
||||||
private sealed record RenameAction(string Name);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 表示没有匹配 reducer 的 action,用于验证无变更分发路径。
|
|
||||||
/// </summary>
|
|
||||||
private sealed record NoopAction;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 表示参与多态匹配测试的 action 标记接口。
|
|
||||||
/// </summary>
|
|
||||||
private interface IIncrementActionMarker
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取增量值。
|
|
||||||
/// </summary>
|
|
||||||
int Amount { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 表示多态匹配测试中的基类 action。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Amount">要增加的数量。</param>
|
|
||||||
private abstract record IncrementActionBase(int Amount);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 表示多态匹配测试中的派生 action。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Amount">要增加的数量。</param>
|
|
||||||
private sealed record DerivedIncrementAction(int Amount)
|
|
||||||
: IncrementActionBase(Amount), IIncrementActionMarker;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 显式选择器实现,用于验证 IStateSelector 重载。
|
|
||||||
/// </summary>
|
|
||||||
private sealed class CounterNameSelector : IStateSelector<CounterState, string>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 从状态中选择名称字段。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state">完整状态。</param>
|
|
||||||
/// <returns>名称字段。</returns>
|
|
||||||
public string Select(CounterState state)
|
|
||||||
{
|
|
||||||
return state.Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将计数值按十位分桶比较的测试比较器。
|
|
||||||
/// 该比较器用于验证选择器只在局部状态“语义变化”时才触发通知。
|
|
||||||
/// </summary>
|
|
||||||
private sealed class TensBucketEqualityComparer : IEqualityComparer<int>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 判断两个值是否落在同一个十位分桶中。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">左侧值。</param>
|
|
||||||
/// <param name="y">右侧值。</param>
|
|
||||||
/// <returns>若位于同一分桶则返回 <see langword="true"/>,否则返回 <see langword="false"/>。</returns>
|
|
||||||
public bool Equals(int x, int y)
|
|
||||||
{
|
|
||||||
return x / 10 == y / 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 返回基于十位分桶的哈希码。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj">目标值。</param>
|
|
||||||
/// <returns>分桶哈希码。</returns>
|
|
||||||
public int GetHashCode(int obj)
|
|
||||||
{
|
|
||||||
return obj / 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用于测试 StoreBuilder 自定义状态比较器的比较器实现。
|
|
||||||
/// 该比较器忽略名称字段的大小写差异,并保持计数字段严格比较。
|
|
||||||
/// </summary>
|
|
||||||
private sealed class CounterStateNameInsensitiveComparer : IEqualityComparer<CounterState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 判断两个状态是否在业务语义上相等。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">左侧状态。</param>
|
|
||||||
/// <param name="y">右侧状态。</param>
|
|
||||||
/// <returns>若两个状态在计数相同且名称仅大小写不同,则返回 <see langword="true"/>。</returns>
|
|
||||||
public bool Equals(CounterState? x, CounterState? y)
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(x, y))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x is null || y is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return x.Count == y.Count &&
|
|
||||||
string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 返回与业务语义一致的哈希码。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj">目标状态。</param>
|
|
||||||
/// <returns>忽略名称大小写后的哈希码。</returns>
|
|
||||||
public int GetHashCode(CounterState obj)
|
|
||||||
{
|
|
||||||
return HashCode.Combine(obj.Count, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 记录中间件调用顺序的测试中间件。
|
|
||||||
/// </summary>
|
|
||||||
private sealed class RecordingMiddleware(List<string> logs, string name) : IStoreMiddleware<CounterState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 记录当前中间件在分发前后的调用顺序。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">当前分发上下文。</param>
|
|
||||||
/// <param name="next">后续处理节点。</param>
|
|
||||||
public void Invoke(StoreDispatchContext<CounterState> context, Action next)
|
|
||||||
{
|
|
||||||
logs.Add($"{name}:before");
|
|
||||||
next();
|
|
||||||
logs.Add($"{name}:after");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用于验证 dispatch 管线在 middleware 执行期间不会占用状态锁的测试中间件。
|
|
||||||
/// </summary>
|
|
||||||
private sealed class BlockingMiddleware(ManualResetEventSlim entered, ManualResetEventSlim release)
|
|
||||||
: IStoreMiddleware<CounterState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 通知测试线程 middleware 已进入阻塞点,并等待释放信号后继续执行。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">当前分发上下文。</param>
|
|
||||||
/// <param name="next">后续处理节点。</param>
|
|
||||||
public void Invoke(StoreDispatchContext<CounterState> context, Action next)
|
|
||||||
{
|
|
||||||
entered.Set();
|
|
||||||
release.Wait(TimeSpan.FromSeconds(2));
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 在中间件阶段尝试二次分发的测试中间件,用于验证重入保护。
|
|
||||||
/// </summary>
|
|
||||||
private sealed class NestedDispatchMiddleware(Store<CounterState> store) : IStoreMiddleware<CounterState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 标记是否已经触发过一次嵌套分发,避免因测试实现本身导致无限递归。
|
|
||||||
/// </summary>
|
|
||||||
private bool _hasTriggered;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 在第一次进入中间件时执行嵌套分发。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">当前分发上下文。</param>
|
|
||||||
/// <param name="next">后续处理节点。</param>
|
|
||||||
public void Invoke(StoreDispatchContext<CounterState> context, Action next)
|
|
||||||
{
|
|
||||||
if (!_hasTriggered)
|
|
||||||
{
|
|
||||||
_hasTriggered = true;
|
|
||||||
store.Dispatch(new IncrementAction(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
using GFramework.Core.Abstractions.Lifecycle;
|
using GFramework.Core.Abstractions.Lifecycle;
|
||||||
using GFramework.Core.Abstractions.Systems;
|
using GFramework.Core.Abstractions.Systems;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
using GFramework.Core.Abstractions.Systems;
|
using GFramework.Core.Abstractions.Systems;
|
||||||
|
|
||||||
|
|||||||
@ -1,129 +0,0 @@
|
|||||||
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,11 +1,14 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
using GFramework.Core.Abstractions.Environment;
|
using GFramework.Core.Abstractions.Environment;
|
||||||
|
using GFramework.Core.Abstractions.Ioc;
|
||||||
|
using GFramework.Core.Abstractions.Lifecycle;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Abstractions.Model;
|
using GFramework.Core.Abstractions.Model;
|
||||||
using GFramework.Core.Abstractions.Systems;
|
using GFramework.Core.Abstractions.Systems;
|
||||||
using GFramework.Core.Abstractions.Utility;
|
using GFramework.Core.Abstractions.Utility;
|
||||||
using GFramework.Core.Environment;
|
using GFramework.Core.Environment;
|
||||||
|
using GFramework.Core.Extensions;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@ -14,138 +17,15 @@ namespace GFramework.Core.Architectures;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 架构基类,提供系统、模型、工具等组件的注册与管理功能。
|
/// 架构基类,提供系统、模型、工具等组件的注册与管理功能。
|
||||||
/// 专注于生命周期管理、初始化流程控制和架构阶段转换。
|
/// 专注于生命周期管理、初始化流程控制和架构阶段转换。
|
||||||
///
|
|
||||||
/// 重构说明:此类已重构为协调器模式,将职责委托给专门的管理器:
|
|
||||||
/// - ArchitectureLifecycle: 生命周期管理
|
|
||||||
/// - ArchitectureComponentRegistry: 组件注册管理
|
|
||||||
/// - ArchitectureModules: 模块管理
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Architecture : IArchitecture
|
public abstract class Architecture(
|
||||||
{
|
|
||||||
#region Constructor
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 构造函数,初始化架构和管理器
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="configuration">架构配置</param>
|
|
||||||
/// <param name="environment">环境配置</param>
|
|
||||||
/// <param name="services">服务管理器</param>
|
|
||||||
/// <param name="context">架构上下文</param>
|
|
||||||
protected Architecture(
|
|
||||||
IArchitectureConfiguration? configuration = null,
|
IArchitectureConfiguration? configuration = null,
|
||||||
IEnvironment? environment = null,
|
IEnvironment? environment = null,
|
||||||
IArchitectureServices? services = null,
|
IArchitectureServices? services = null,
|
||||||
IArchitectureContext? context = null)
|
IArchitectureContext? context = null
|
||||||
{
|
)
|
||||||
Configuration = configuration ?? new ArchitectureConfiguration();
|
: IArchitecture
|
||||||
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="hook">生命周期钩子实例</param>
|
|
||||||
/// <returns>注册的钩子实例</returns>
|
|
||||||
public IArchitectureLifecycleHook RegisterLifecycleHook(IArchitectureLifecycleHook hook)
|
|
||||||
{
|
|
||||||
return _lifecycle.RegisterLifecycleHook(hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取架构配置对象
|
|
||||||
/// </summary>
|
|
||||||
private IArchitectureConfiguration Configuration { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取环境配置对象
|
|
||||||
/// </summary>
|
|
||||||
private IEnvironment Environment { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取服务管理器
|
|
||||||
/// </summary>
|
|
||||||
private IArchitectureServices Services { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 当前架构的阶段
|
|
||||||
/// </summary>
|
|
||||||
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
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 日志记录器实例
|
|
||||||
/// </summary>
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 架构上下文实例
|
|
||||||
/// </summary>
|
|
||||||
private IArchitectureContext? _context;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 生命周期管理器
|
|
||||||
/// </summary>
|
|
||||||
private readonly ArchitectureLifecycle _lifecycle;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 组件注册管理器
|
|
||||||
/// </summary>
|
|
||||||
private readonly ArchitectureComponentRegistry _componentRegistry;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 模块管理器
|
|
||||||
/// </summary>
|
|
||||||
private readonly ArchitectureModules _modules;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Module Management
|
#region Module Management
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -155,7 +35,8 @@ public abstract class Architecture : IArchitecture
|
|||||||
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
|
||||||
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
|
||||||
{
|
{
|
||||||
_modules.RegisterMediatorBehavior<TBehavior>();
|
_logger.Debug($"Registering mediator behavior: {typeof(TBehavior).Name}");
|
||||||
|
Container.RegisterMediatorBehavior<TBehavior>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -165,7 +46,376 @@ public abstract class Architecture : IArchitecture
|
|||||||
/// <returns>安装的模块实例</returns>
|
/// <returns>安装的模块实例</returns>
|
||||||
public IArchitectureModule InstallModule(IArchitectureModule module)
|
public IArchitectureModule InstallModule(IArchitectureModule module)
|
||||||
{
|
{
|
||||||
return _modules.InstallModule(module);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取架构配置对象
|
||||||
|
/// </summary>
|
||||||
|
private IArchitectureConfiguration Configuration { get; } = configuration ?? new ArchitectureConfiguration();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取环境配置对象
|
||||||
|
/// </summary>
|
||||||
|
private IEnvironment Environment { get; } = environment ?? new DefaultEnvironment();
|
||||||
|
|
||||||
|
private IArchitectureServices Services { get; } = services ?? new ArchitectureServices();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取依赖注入容器
|
||||||
|
/// </summary>
|
||||||
|
private IIocContainer Container => Services.Container;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前架构的阶段
|
||||||
|
/// </summary>
|
||||||
|
public ArchitecturePhase CurrentPhase { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 架构上下文
|
||||||
|
/// </summary>
|
||||||
|
public IArchitectureContext Context => _context!;
|
||||||
|
|
||||||
|
#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!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 架构上下文实例
|
||||||
|
/// </summary>
|
||||||
|
private IArchitectureContext? _context = context;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Lifecycle Management
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进入指定的架构阶段,并执行相应的生命周期管理操作
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="next">要进入的下一个架构阶段</param>
|
||||||
|
/// <exception cref="InvalidOperationException">当阶段转换不被允许时抛出异常</exception>
|
||||||
|
protected virtual 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -173,77 +423,177 @@ public abstract class Architecture : IArchitecture
|
|||||||
#region Component Registration
|
#region Component Registration
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册一个系统到架构中
|
/// 验证是否允许注册组件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TSystem">要注册的系统类型</typeparam>
|
/// <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>
|
||||||
/// <param name="system">要注册的系统实例</param>
|
/// <param name="system">要注册的系统实例</param>
|
||||||
/// <returns>注册成功的系统实例</returns>
|
/// <returns>注册成功的系统实例</returns>
|
||||||
public TSystem RegisterSystem<TSystem>(TSystem system) where TSystem : ISystem
|
public TSystem RegisterSystem<TSystem>(TSystem system) where TSystem : ISystem
|
||||||
{
|
{
|
||||||
return _componentRegistry.RegisterSystem(system);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册系统类型,由 DI 容器自动创建实例
|
/// 注册系统类型,由 DI 容器自动创建实例
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">系统类型</typeparam>
|
/// <typeparam name="T">系统类型</typeparam>
|
||||||
/// <param name="onCreated">可选的实例创建后回调</param>
|
/// <param name="onCreated">可选的实例创建后回调,用于自定义配置</param>
|
||||||
public void RegisterSystem<T>(Action<T>? onCreated = null) where T : class, ISystem
|
public void RegisterSystem<T>(Action<T>? onCreated = null) where T : class, ISystem
|
||||||
{
|
{
|
||||||
_componentRegistry.RegisterSystem(onCreated);
|
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}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册一个模型到架构中
|
/// 注册一个模型到架构中。
|
||||||
|
/// 若当前未初始化,则暂存至待初始化列表;否则立即初始化该模型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TModel">要注册的模型类型</typeparam>
|
/// <typeparam name="TModel">要注册的模型类型,必须实现IModel接口</typeparam>
|
||||||
/// <param name="model">要注册的模型实例</param>
|
/// <param name="model">要注册的模型实例</param>
|
||||||
/// <returns>注册成功的模型实例</returns>
|
/// <returns>注册成功的模型实例</returns>
|
||||||
public TModel RegisterModel<TModel>(TModel model) where TModel : IModel
|
public TModel RegisterModel<TModel>(TModel model) where TModel : IModel
|
||||||
{
|
{
|
||||||
return _componentRegistry.RegisterModel(model);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册模型类型,由 DI 容器自动创建实例
|
/// 注册模型类型,由 DI 容器自动创建实例
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">模型类型</typeparam>
|
/// <typeparam name="T">模型类型</typeparam>
|
||||||
/// <param name="onCreated">可选的实例创建后回调</param>
|
/// <param name="onCreated">可选的实例创建后回调,用于自定义配置</param>
|
||||||
public void RegisterModel<T>(Action<T>? onCreated = null) where T : class, IModel
|
public void RegisterModel<T>(Action<T>? onCreated = null) where T : class, IModel
|
||||||
{
|
{
|
||||||
_componentRegistry.RegisterModel(onCreated);
|
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}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册一个工具到架构中
|
/// 注册一个工具到架构中
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TUtility">要注册的工具类型</typeparam>
|
/// <typeparam name="TUtility">要注册的工具类型,必须实现IUtility接口</typeparam>
|
||||||
/// <param name="utility">要注册的工具实例</param>
|
/// <param name="utility">要注册的工具实例</param>
|
||||||
/// <returns>注册成功的工具实例</returns>
|
/// <returns>注册成功的工具实例</returns>
|
||||||
public TUtility RegisterUtility<TUtility>(TUtility utility) where TUtility : IUtility
|
public TUtility RegisterUtility<TUtility>(TUtility utility) where TUtility : IUtility
|
||||||
{
|
{
|
||||||
return _componentRegistry.RegisterUtility(utility);
|
_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册工具类型,由 DI 容器自动创建实例
|
/// 注册工具类型,由 DI 容器自动创建实例
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">工具类型</typeparam>
|
/// <typeparam name="T">工具类型</typeparam>
|
||||||
/// <param name="onCreated">可选的实例创建后回调</param>
|
/// <param name="onCreated">可选的实例创建后回调,用于自定义配置</param>
|
||||||
public void RegisterUtility<T>(Action<T>? onCreated = null) where T : class, IUtility
|
public void RegisterUtility<T>(Action<T>? onCreated = null) where T : class, IUtility
|
||||||
{
|
{
|
||||||
_componentRegistry.RegisterUtility(onCreated);
|
_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}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Initialization
|
#region Initialization
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 抽象初始化方法,由子类重写以进行自定义初始化操作
|
|
||||||
/// </summary>
|
|
||||||
protected abstract void OnInitialize();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 同步初始化方法,阻塞当前线程直到初始化完成
|
/// 同步初始化方法,阻塞当前线程直到初始化完成
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -256,7 +606,7 @@ public abstract class Architecture : IArchitecture
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.Error("Architecture initialization failed:", e);
|
_logger.Error("Architecture initialization failed:", e);
|
||||||
_lifecycle.MarkAsFailed(e);
|
EnterPhase(ArchitecturePhase.FailedInitialization);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,6 +614,7 @@ public abstract class Architecture : IArchitecture
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步初始化方法,返回Task以便调用者可以等待初始化完成
|
/// 异步初始化方法,返回Task以便调用者可以等待初始化完成
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>表示异步初始化操作的Task</returns>
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -273,29 +624,32 @@ public abstract class Architecture : IArchitecture
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.Error("Architecture initialization failed:", e);
|
_logger.Error("Architecture initialization failed:", e);
|
||||||
_lifecycle.MarkAsFailed(e);
|
EnterPhase(ArchitecturePhase.FailedInitialization);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步初始化架构内部组件
|
/// 异步初始化架构内部组件,包括上下文、模型和系统的初始化
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="asyncMode">是否启用异步模式</param>
|
/// <param name="asyncMode">是否启用异步模式进行组件初始化</param>
|
||||||
|
/// <returns>异步任务,表示初始化操作的完成</returns>
|
||||||
private async Task InitializeInternalAsync(bool asyncMode)
|
private async Task InitializeInternalAsync(bool asyncMode)
|
||||||
{
|
{
|
||||||
// === 基础环境初始化 ===
|
// === 基础上下文 & Logger ===
|
||||||
|
LoggerFactoryResolver.Provider = Configuration.LoggerProperties.LoggerFactoryProvider;
|
||||||
|
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
|
||||||
Environment.Initialize();
|
Environment.Initialize();
|
||||||
|
|
||||||
// 注册内置服务模块
|
// 注册内置服务模块
|
||||||
Services.ModuleManager.RegisterBuiltInModules(Services.Container);
|
Services.ModuleManager.RegisterBuiltInModules(Container);
|
||||||
|
|
||||||
// 将 Environment 注册到容器
|
// 将 Environment 注册到容器(如果尚未注册)
|
||||||
if (!Services.Container.Contains<IEnvironment>())
|
if (!Container.Contains<IEnvironment>())
|
||||||
Services.Container.RegisterPlurality(Environment);
|
Container.RegisterPlurality(Environment);
|
||||||
|
|
||||||
// 初始化架构上下文
|
// 初始化架构上下文(如果尚未初始化)
|
||||||
_context ??= new ArchitectureContext(Services.Container);
|
_context ??= new ArchitectureContext(Container);
|
||||||
GameContext.Bind(GetType(), _context);
|
GameContext.Bind(GetType(), _context);
|
||||||
|
|
||||||
// 为服务设置上下文
|
// 为服务设置上下文
|
||||||
@ -306,7 +660,7 @@ public abstract class Architecture : IArchitecture
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 执行服务钩子
|
// 执行服务钩子
|
||||||
Services.Container.ExecuteServicesHook(Configurator);
|
Container.ExecuteServicesHook(Configurator);
|
||||||
|
|
||||||
// 初始化服务模块
|
// 初始化服务模块
|
||||||
await Services.ModuleManager.InitializeAllAsync(asyncMode);
|
await Services.ModuleManager.InitializeAllAsync(asyncMode);
|
||||||
@ -317,44 +671,39 @@ public abstract class Architecture : IArchitecture
|
|||||||
_logger.Debug("User OnInitialize() completed");
|
_logger.Debug("User OnInitialize() completed");
|
||||||
|
|
||||||
// === 组件初始化阶段 ===
|
// === 组件初始化阶段 ===
|
||||||
await _lifecycle.InitializeAllComponentsAsync(asyncMode);
|
await InitializeAllComponentsAsync(asyncMode);
|
||||||
|
|
||||||
// === 初始化完成阶段 ===
|
// === 初始化完成阶段 ===
|
||||||
Services.Container.Freeze();
|
Container.Freeze();
|
||||||
_logger.Info("IOC container frozen");
|
_logger.Info("IOC container frozen");
|
||||||
|
|
||||||
_lifecycle.MarkAsReady();
|
_mInitialized = true;
|
||||||
|
EnterPhase(ArchitecturePhase.Ready);
|
||||||
|
// 🔥 释放 Ready await
|
||||||
|
_readyTcs.TrySetResult();
|
||||||
|
|
||||||
_logger.Info($"Architecture {GetType().Name} is ready - all components initialized");
|
_logger.Info($"Architecture {GetType().Name} is ready - all components initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 等待架构初始化完成(Ready 阶段)
|
/// 等待架构初始化完成(Ready 阶段)
|
||||||
|
/// 如果架构已经处于就绪状态,则立即返回已完成的任务;
|
||||||
|
/// 否则返回一个任务,该任务将在架构进入就绪状态时完成。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>表示等待操作的Task对象</returns>
|
||||||
public Task WaitUntilReadyAsync()
|
public Task WaitUntilReadyAsync()
|
||||||
{
|
{
|
||||||
return _lifecycle.WaitUntilReadyAsync();
|
return IsReady ? Task.CompletedTask : _readyTcs.Task;
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Destruction
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 异步销毁架构及所有组件
|
|
||||||
/// </summary>
|
|
||||||
public virtual async ValueTask DestroyAsync()
|
|
||||||
{
|
|
||||||
await _lifecycle.DestroyAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 销毁架构并清理所有组件资源(同步方法,保留用于向后兼容)
|
/// 获取用于配置服务集合的委托
|
||||||
|
/// 默认实现返回null,子类可以重写此属性以提供自定义配置逻辑
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("建议使用 DestroyAsync() 以支持异步清理")]
|
/// <value>
|
||||||
public virtual void Destroy()
|
/// 一个可为空的Action委托,用于配置IServiceCollection实例
|
||||||
{
|
/// </value>
|
||||||
_lifecycle.Destroy();
|
public virtual Action<IServiceCollection>? Configurator => null;
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@ -1,204 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Abstractions.Properties;
|
using GFramework.Core.Abstractions.Properties;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Command;
|
using GFramework.Core.Abstractions.Command;
|
||||||
using GFramework.Core.Abstractions.Environment;
|
using GFramework.Core.Abstractions.Environment;
|
||||||
using GFramework.Core.Abstractions.Events;
|
using GFramework.Core.Abstractions.Events;
|
||||||
|
|||||||
@ -1,420 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Command;
|
using GFramework.Core.Abstractions.Command;
|
||||||
using GFramework.Core.Abstractions.Events;
|
using GFramework.Core.Abstractions.Events;
|
||||||
using GFramework.Core.Abstractions.Ioc;
|
using GFramework.Core.Abstractions.Ioc;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
namespace GFramework.Core.Architectures;
|
namespace GFramework.Core.Architectures;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
namespace GFramework.Core.Architectures;
|
namespace GFramework.Core.Architectures;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
using GFramework.Core.Abstractions.Enums;
|
using GFramework.Core.Abstractions.Enums;
|
||||||
using GFramework.Core.Abstractions.Utility;
|
using GFramework.Core.Abstractions.Utility;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
using GFramework.Core.Abstractions.Architectures;
|
using GFramework.Core.Abstractions.Architecture;
|
||||||
|
|
||||||
namespace GFramework.Core.Architectures;
|
namespace GFramework.Core.Architectures;
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,11 @@ internal sealed class CoroutineSlot
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public CoroutineHandle Handle;
|
public CoroutineHandle Handle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 协程是否已经开始执行
|
||||||
|
/// </summary>
|
||||||
|
public bool HasStarted;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 协程的优先级
|
/// 协程的优先级
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -30,6 +30,12 @@ public sealed class WaitForAllCoroutines(
|
|||||||
/// 获取一个值,指示所有协程是否已完成执行
|
/// 获取一个值,指示所有协程是否已完成执行
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>当所有协程都已完成时返回true,否则返回false</returns>
|
/// <returns>当所有协程都已完成时返回true,否则返回false</returns>
|
||||||
|
public bool IsDone
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
// 检查所有协程句柄是否都不在调度器中存活
|
// 检查所有协程句柄是否都不在调度器中存活
|
||||||
public bool IsDone => _handles.All(handle => !_scheduler.IsCoroutineAlive(handle));
|
return _handles.All(handle => !_scheduler.IsCoroutineAlive(handle));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -18,11 +18,6 @@ public class PriorityEvent<T> : IEvent
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly List<EventHandler> _handlers = new();
|
private readonly List<EventHandler> _handlers = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 保护处理器集合的并发访问
|
|
||||||
/// </summary>
|
|
||||||
private readonly object _syncRoot = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 标记事件是否已被处理(用于 UntilHandled 传播模式)
|
/// 标记事件是否已被处理(用于 UntilHandled 传播模式)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -57,13 +52,10 @@ public class PriorityEvent<T> : IEvent
|
|||||||
public IUnRegister Register(Action<T> onEvent, int priority)
|
public IUnRegister Register(Action<T> onEvent, int priority)
|
||||||
{
|
{
|
||||||
var handler = new EventHandler(onEvent, priority);
|
var handler = new EventHandler(onEvent, priority);
|
||||||
lock (_syncRoot)
|
|
||||||
{
|
|
||||||
_handlers.Add(handler);
|
_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));
|
return new DefaultUnRegister(() => UnRegister(onEvent));
|
||||||
}
|
}
|
||||||
@ -73,12 +65,9 @@ public class PriorityEvent<T> : IEvent
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="onEvent">需要被注销的事件处理方法</param>
|
/// <param name="onEvent">需要被注销的事件处理方法</param>
|
||||||
public void UnRegister(Action<T> onEvent)
|
public void UnRegister(Action<T> onEvent)
|
||||||
{
|
|
||||||
lock (_syncRoot)
|
|
||||||
{
|
{
|
||||||
_handlers.RemoveAll(h => h.Handler == onEvent);
|
_handlers.RemoveAll(h => h.Handler == onEvent);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册一个上下文事件监听器,并指定优先级
|
/// 注册一个上下文事件监听器,并指定优先级
|
||||||
@ -89,13 +78,10 @@ public class PriorityEvent<T> : IEvent
|
|||||||
public IUnRegister RegisterWithContext(Action<EventContext<T>> onEvent, int priority = 0)
|
public IUnRegister RegisterWithContext(Action<EventContext<T>> onEvent, int priority = 0)
|
||||||
{
|
{
|
||||||
var handler = new ContextEventHandler(onEvent, priority);
|
var handler = new ContextEventHandler(onEvent, priority);
|
||||||
lock (_syncRoot)
|
|
||||||
{
|
|
||||||
_contextHandlers.Add(handler);
|
_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));
|
return new DefaultUnRegister(() => UnRegisterContext(onEvent));
|
||||||
}
|
}
|
||||||
@ -105,12 +91,9 @@ public class PriorityEvent<T> : IEvent
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="onEvent">需要被注销的事件处理方法</param>
|
/// <param name="onEvent">需要被注销的事件处理方法</param>
|
||||||
public void UnRegisterContext(Action<EventContext<T>> onEvent)
|
public void UnRegisterContext(Action<EventContext<T>> onEvent)
|
||||||
{
|
|
||||||
lock (_syncRoot)
|
|
||||||
{
|
{
|
||||||
_contextHandlers.RemoveAll(h => h.Handler == onEvent);
|
_contextHandlers.RemoveAll(h => h.Handler == onEvent);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 触发事件处理程序,并指定传播模式
|
/// 触发事件处理程序,并指定传播模式
|
||||||
@ -189,7 +172,8 @@ public class PriorityEvent<T> : IEvent
|
|||||||
/// <param name="t">事件参数</param>
|
/// <param name="t">事件参数</param>
|
||||||
private void TriggerHighest(T t)
|
private void TriggerHighest(T t)
|
||||||
{
|
{
|
||||||
var (normalSnapshot, contextSnapshot) = CreateSnapshots();
|
var normalSnapshot = _handlers.ToArray();
|
||||||
|
var contextSnapshot = _contextHandlers.ToArray();
|
||||||
var highestPriority = GetHighestPriority(normalSnapshot, contextSnapshot);
|
var highestPriority = GetHighestPriority(normalSnapshot, contextSnapshot);
|
||||||
|
|
||||||
if (highestPriority != int.MinValue)
|
if (highestPriority != int.MinValue)
|
||||||
@ -207,11 +191,15 @@ public class PriorityEvent<T> : IEvent
|
|||||||
private List<(int Priority, Action? Handler, Action<EventContext<T>>? ContextHandler, bool IsContext)>
|
private List<(int Priority, Action? Handler, Action<EventContext<T>>? ContextHandler, bool IsContext)>
|
||||||
MergeAndSortHandlers(T t)
|
MergeAndSortHandlers(T t)
|
||||||
{
|
{
|
||||||
var (normalSnapshot, contextSnapshot) = CreateSnapshots();
|
var normalSnapshot = _handlers.ToArray();
|
||||||
// 使用统一的投影方法显式固定元组的可空标注,避免 LINQ 在 Concat 时推断出不兼容的签名。
|
var contextSnapshot = _contextHandlers.ToArray();
|
||||||
|
// 使用快照避免迭代期间修改
|
||||||
return normalSnapshot
|
return normalSnapshot
|
||||||
.Select(h => CreateNormalHandlerInvocation(h, t))
|
.Select(h => (h.Priority, Handler: (Action?)(() => h.Handler.Invoke(t)),
|
||||||
.Concat(contextSnapshot.Select(CreateContextHandlerInvocation))
|
ContextHandler: (Action<EventContext<T>>?)null, IsContext: false))
|
||||||
|
.Concat(contextSnapshot
|
||||||
|
.Select(h => (h.Priority, Handler: (Action?)null,
|
||||||
|
ContextHandler: (Action<EventContext<T>>?)h.Handler, IsContext: true)))
|
||||||
.OrderByDescending(h => h.Priority)
|
.OrderByDescending(h => h.Priority)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@ -271,43 +259,9 @@ public class PriorityEvent<T> : IEvent
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>监听器总数量</returns>
|
/// <returns>监听器总数量</returns>
|
||||||
public int GetListenerCount()
|
public int GetListenerCount()
|
||||||
{
|
|
||||||
lock (_syncRoot)
|
|
||||||
{
|
{
|
||||||
return _handlers.Count + _contextHandlers.Count;
|
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>
|
/// <summary>
|
||||||
/// 事件处理器包装类,包含处理器和优先级
|
/// 事件处理器包装类,包含处理器和优先级
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
using GFramework.Core.Abstractions.Events;
|
|
||||||
using GFramework.Core.Abstractions.StateManagement;
|
|
||||||
using GFramework.Core.Events;
|
|
||||||
using GFramework.Core.StateManagement;
|
|
||||||
|
|
||||||
namespace GFramework.Core.Extensions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 为 Store 提供到 EventBus 的兼容桥接扩展。
|
|
||||||
/// 该扩展面向旧模块渐进迁移场景,使现有事件消费者可以继续观察 Store 的 action 分发和状态变化。
|
|
||||||
/// </summary>
|
|
||||||
public static class StoreEventBusExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 将 Store 的 dispatch 和状态变化同时桥接到 EventBus。
|
|
||||||
/// dispatch 事件会逐次发布;状态变化事件会复用 Store 自身的通知折叠语义,因此批处理中只发布最终状态。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
/// <param name="store">源 Store。</param>
|
|
||||||
/// <param name="eventBus">目标事件总线。</param>
|
|
||||||
/// <param name="publishDispatches">是否发布每次 action 分发事件。</param>
|
|
||||||
/// <param name="publishStateChanges">是否发布状态变化事件。</param>
|
|
||||||
/// <returns>用于拆除桥接的句柄。</returns>
|
|
||||||
public static IUnRegister BridgeToEventBus<TState>(
|
|
||||||
this Store<TState> store,
|
|
||||||
IEventBus eventBus,
|
|
||||||
bool publishDispatches = true,
|
|
||||||
bool publishStateChanges = true)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(store);
|
|
||||||
ArgumentNullException.ThrowIfNull(eventBus);
|
|
||||||
|
|
||||||
IUnRegister? dispatchBridge = null;
|
|
||||||
IUnRegister? stateBridge = null;
|
|
||||||
|
|
||||||
if (publishDispatches)
|
|
||||||
{
|
|
||||||
dispatchBridge = store.BridgeDispatchesToEventBus(eventBus);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (publishStateChanges)
|
|
||||||
{
|
|
||||||
stateBridge = store.BridgeStateChangesToEventBus(eventBus);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DefaultUnRegister(() =>
|
|
||||||
{
|
|
||||||
dispatchBridge?.UnRegister();
|
|
||||||
stateBridge?.UnRegister();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将 Store 的每次 dispatch 结果桥接到 EventBus。
|
|
||||||
/// 该桥接通过中间件实现,因此即使某次分发未改变状态,也会发布对应的 dispatch 事件。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
/// <param name="store">源 Store。</param>
|
|
||||||
/// <param name="eventBus">目标事件总线。</param>
|
|
||||||
/// <returns>用于移除 dispatch 桥接中间件的句柄。</returns>
|
|
||||||
public static IUnRegister BridgeDispatchesToEventBus<TState>(this Store<TState> store, IEventBus eventBus)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(store);
|
|
||||||
ArgumentNullException.ThrowIfNull(eventBus);
|
|
||||||
|
|
||||||
return store.RegisterMiddleware(new DispatchEventBusMiddleware<TState>(eventBus));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将 Store 的状态变化桥接到 EventBus。
|
|
||||||
/// 该桥接复用 Store 的订阅通知语义,因此只会在状态真正变化时发布事件。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
/// <param name="store">源 Store。</param>
|
|
||||||
/// <param name="eventBus">目标事件总线。</param>
|
|
||||||
/// <returns>用于移除状态变化桥接的句柄。</returns>
|
|
||||||
public static IUnRegister BridgeStateChangesToEventBus<TState>(this IReadonlyStore<TState> store,
|
|
||||||
IEventBus eventBus)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(store);
|
|
||||||
ArgumentNullException.ThrowIfNull(eventBus);
|
|
||||||
|
|
||||||
return store.Subscribe(state =>
|
|
||||||
eventBus.Send(new StoreStateChangedEvent<TState>(state, DateTimeOffset.UtcNow)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用于把 dispatch 结果桥接到 EventBus 的内部中间件。
|
|
||||||
/// 选择中间件而不是改写 Store 核心提交流程,是为了把兼容层成本保持在可选扩展中。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">状态树的根状态类型。</typeparam>
|
|
||||||
private sealed class DispatchEventBusMiddleware<TState>(IEventBus eventBus) : IStoreMiddleware<TState>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 目标事件总线。
|
|
||||||
/// </summary>
|
|
||||||
private readonly IEventBus _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 执行后续 dispatch 管线,并在结束后把分发结果发送到 EventBus。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">当前分发上下文。</param>
|
|
||||||
/// <param name="next">后续管线。</param>
|
|
||||||
public void Invoke(StoreDispatchContext<TState> context, Action next)
|
|
||||||
{
|
|
||||||
next();
|
|
||||||
|
|
||||||
var dispatchRecord = new StoreDispatchRecord<TState>(
|
|
||||||
context.Action,
|
|
||||||
context.PreviousState,
|
|
||||||
context.NextState,
|
|
||||||
context.HasStateChanged,
|
|
||||||
context.DispatchedAt);
|
|
||||||
|
|
||||||
_eventBus.Send(new StoreDispatchedEvent<TState>(dispatchRecord));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
using GFramework.Core.Abstractions.Property;
|
|
||||||
using GFramework.Core.Abstractions.StateManagement;
|
|
||||||
using GFramework.Core.StateManagement;
|
|
||||||
|
|
||||||
namespace GFramework.Core.Extensions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 为 Store 提供选择器和 BindableProperty 风格桥接扩展。
|
|
||||||
/// 这些扩展用于在集中式状态容器和现有 Property/UI 生态之间建立最小侵入的互操作层。
|
|
||||||
/// </summary>
|
|
||||||
public static class StoreExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 从 Store 中选择一个局部状态视图。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
|
||||||
/// <typeparam name="TSelected">局部状态类型。</typeparam>
|
|
||||||
/// <param name="store">源 Store。</param>
|
|
||||||
/// <param name="selector">状态选择委托。</param>
|
|
||||||
/// <returns>可用于订阅局部状态变化的只读绑定视图。</returns>
|
|
||||||
public static StoreSelection<TState, TSelected> Select<TState, TSelected>(
|
|
||||||
this IReadonlyStore<TState> store,
|
|
||||||
Func<TState, TSelected> selector)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(store);
|
|
||||||
ArgumentNullException.ThrowIfNull(selector);
|
|
||||||
|
|
||||||
return new StoreSelection<TState, TSelected>(store, selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从 Store 中选择一个局部状态视图,并指定局部状态比较器。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
|
||||||
/// <typeparam name="TSelected">局部状态类型。</typeparam>
|
|
||||||
/// <param name="store">源 Store。</param>
|
|
||||||
/// <param name="selector">状态选择委托。</param>
|
|
||||||
/// <param name="comparer">用于比较局部状态是否变化的比较器。</param>
|
|
||||||
/// <returns>可用于订阅局部状态变化的只读绑定视图。</returns>
|
|
||||||
public static StoreSelection<TState, TSelected> Select<TState, TSelected>(
|
|
||||||
this IReadonlyStore<TState> store,
|
|
||||||
Func<TState, TSelected> selector,
|
|
||||||
IEqualityComparer<TSelected>? comparer)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(store);
|
|
||||||
ArgumentNullException.ThrowIfNull(selector);
|
|
||||||
|
|
||||||
return new StoreSelection<TState, TSelected>(store, selector, comparer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 使用显式选择器对象从 Store 中选择一个局部状态视图。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
|
||||||
/// <typeparam name="TSelected">局部状态类型。</typeparam>
|
|
||||||
/// <param name="store">源 Store。</param>
|
|
||||||
/// <param name="selector">状态选择器实例。</param>
|
|
||||||
/// <param name="comparer">用于比较局部状态是否变化的比较器。</param>
|
|
||||||
/// <returns>可用于订阅局部状态变化的只读绑定视图。</returns>
|
|
||||||
public static StoreSelection<TState, TSelected> Select<TState, TSelected>(
|
|
||||||
this IReadonlyStore<TState> store,
|
|
||||||
IStateSelector<TState, TSelected> selector,
|
|
||||||
IEqualityComparer<TSelected>? comparer = null)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(store);
|
|
||||||
ArgumentNullException.ThrowIfNull(selector);
|
|
||||||
|
|
||||||
return new StoreSelection<TState, TSelected>(store, selector.Select, comparer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将 Store 中选中的局部状态桥接为 IReadonlyBindableProperty 风格接口。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TState">源状态类型。</typeparam>
|
|
||||||
/// <typeparam name="TSelected">局部状态类型。</typeparam>
|
|
||||||
/// <param name="store">源 Store。</param>
|
|
||||||
/// <param name="selector">状态选择委托。</param>
|
|
||||||
/// <param name="comparer">用于比较局部状态是否变化的比较器。</param>
|
|
||||||
/// <returns>只读绑定属性视图。</returns>
|
|
||||||
public static IReadonlyBindableProperty<TSelected> ToBindableProperty<TState, TSelected>(
|
|
||||||
this IReadonlyStore<TState> store,
|
|
||||||
Func<TState, TSelected> selector,
|
|
||||||
IEqualityComparer<TSelected>? comparer = null)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(store);
|
|
||||||
ArgumentNullException.ThrowIfNull(selector);
|
|
||||||
|
|
||||||
return new StoreSelection<TState, TSelected>(store, selector, comparer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -11,7 +11,7 @@
|
|||||||
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
|
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5"/>
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4"/>
|
||||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0"/>
|
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -16,4 +16,3 @@ global using System.Collections.Generic;
|
|||||||
global using System.Linq;
|
global using System.Linq;
|
||||||
global using System.Threading;
|
global using System.Threading;
|
||||||
global using System.Threading.Tasks;
|
global using System.Threading.Tasks;
|
||||||
global using System.Threading.Channels;
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,317 +0,0 @@
|
|||||||
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
|
|
||||||
{
|
|
||||||
// 忽略回调异常
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,237 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user