diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 1cbb2509..f84bdadd 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -1,106 +1,59 @@ -# 工作流名称:Publish Docs -# 该工作流用于在推送到 main 分支时自动构建并发布文档到 GitHub Pages。 -# 暂时关了,目前这个文档生成有问题,以后再改 +# 工作流名称:Publish Docs (VitePress) name: Publish Docs -# 触发条件: -# 定义工作流的触发规则。当代码仓库发生特定事件时,将自动执行相关操作。 -# 此处配置表示:当向仓库推送(push)带有标签(tag)的提交时触发工作流。 -# 标签匹配规则为通配符 '*',即任意标签都会触发。 on: -# push: -# branches: -# - '**' -# tags: -# - '*' - workflow_dispatch: - -# 权限配置:指定工作流所需的权限。 -# actions: read - 允许读取 GitHub Actions 相关信息。 -# pages: write - 允许写入 GitHub Pages 内容。 -# id-token: write - 允许写入身份令牌(用于部署认证)。 + push: + tags: + - '*' permissions: - actions: read + contents: read pages: write id-token: write -# 并发控制:确保同一组任务不会同时运行。 -# group: "pages" - 将并发任务分组为 "pages"。 -# cancel-in-progress: false - 不取消正在进行的任务。 concurrency: group: "pages" cancel-in-progress: false -# 定义工作流中的作业。 -# if 条件表达式用于判断是否执行该作业。 -# 表达式逻辑如下: -# 1. startsWith(github.ref, 'refs/tags/'):检查当前引用是否以 'refs/tags/' 开头,即是否为标签推送。 -# 2. contains(github.event.head_commit.message, '[release doc]'):检查提交信息中是否包含 '[release doc]' 字符串。 -# 若任一条件满足,则执行该作业。 jobs: - # 作业名称:publish-docs,负责构建和发布文档。 - publish-docs: - if: | - startsWith(github.ref, 'refs/tags/') - || contains(github.event.head_commit.message, '[release doc]') - # 运行环境:使用最新版本的 Ubuntu 虚拟机。 + build-and-deploy: runs-on: ubuntu-latest - # 环境配置:指定部署的目标环境为 github-pages,并设置页面 URL。 + environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - # 步骤定义:按顺序执行一系列操作以完成文档构建与发布。 steps: - # 步骤 1:检出代码仓库。 + # 1️⃣ 拉取仓库代码 - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v4 - # 步骤 2:安装 .NET SDK。 - - name: Setup .NET - uses: actions/setup-dotnet@v5 + # 2️⃣ 安装 Node.js + - name: Setup Node + uses: actions/setup-node@v4 with: - dotnet-version: 10.0.x - - # 步骤 3:恢复项目依赖项。 - - name: Restore - run: dotnet restore - - # 步骤 4:构建项目并生成 XML 文档。 - - name: Build (generate XML docs) - run: dotnet build -c Release + node-version: 20 + cache: 'npm' - # 步骤 5:安装 DocFX 工具。 - - name: Install DocFX - run: dotnet tool update -g docfx - - # 步骤 6:使用 DocFX 构建静态站点。 - - name: Build DocFX + # 3️⃣ 安装依赖 + - name: Install Dependencies run: | - export PATH="$PATH:$HOME/.dotnet/tools" - cd docfx - docfx metadata - docfx build + cd docs + npm install - - name: Debug DocFX output + # 4️⃣ 构建 VitePress + - name: Build VitePress run: | - echo "==== docfx directory ====" - ls -la docfx || true - - echo "==== _site directory ====" - ls -la docfx/_site || echo "_site not found" - - echo "==== _site content ====" - find docfx/_site | head -n 50 || true + cd docs + npm run build - # 步骤 7:上传构建好的静态站点文件作为工件。 + # 5️⃣ 上传构建产物 - name: Upload Pages Artifact uses: actions/upload-pages-artifact@v3 with: - path: docfx/_site + path: docs/.vitepress/dist - # 步骤 8:将静态站点部署到 GitHub Pages。 + # 6️⃣ 部署到 GitHub Pages - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/GFramework.Core.Abstractions/README.md b/GFramework.Core.Abstractions/README.md index 812cba21..dce62d6f 100644 --- a/GFramework.Core.Abstractions/README.md +++ b/GFramework.Core.Abstractions/README.md @@ -1,521 +1,26 @@ -# GFramework.Core.Abstractions 抽象层 +# GFramework.Core.Abstractions -> GFramework 框架的接口定义模块,提供所有核心组件的抽象契约 +GFramework 框架的抽象层定义模块,包含所有核心组件的接口定义。 -## 概述 +## 主要内容 -GFramework.Core.Abstractions 是 GFramework 框架的抽象层定义模块,包含了框架中所有核心组件的接口(Interface)、枚举(Enum)和配置类(Class)。该模块采用 -`netstandard2.0` 作为目标框架,确保了广泛的兼容性和可移植性。 +- 架构核心接口 (IArchitecture, IArchitectureContext等) +- 数据模型接口 (IModel) +- 业务系统接口 (ISystem) +- 控制器接口 (IController) +- 命令与查询接口 (ICommand, IQuery) +- 事件系统接口 (IEvent, IEventBus) +- 依赖注入容器接口 (IIocContainer) +- 可绑定属性接口 (IBindableProperty) +- 日志系统接口 (ILogger) -本模块遵循以下设计原则: +## 设计原则 -- **接口隔离**:每个接口职责单一,便于实现和测试 -- **依赖倒置**:上层模块依赖抽象接口,而非具体实现 -- **组合优于继承**:通过接口组合获得能力,而非通过继承获得 -- **类型安全**:充分利用泛型系统确保类型安全 +- 接口隔离,每个接口职责单一 +- 依赖倒置,上层依赖抽象接口 +- 类型安全,充分利用泛型系统 +- 广泛兼容,基于 netstandard2.0 -本模块的包 ID 为 `GeWuYou.GFramework.Core.Abstractions`,遵循命名空间 `GFramework.Core.Abstractions` 下的所有定义。 +## 详细文档 -## 目录结构 - -``` -GFramework.Core.Abstractions/ -├── architecture/ # 架构核心接口 -│ ├── IArchitecture.cs -│ ├── IArchitectureConfiguration.cs -│ ├── IArchitectureContext.cs -│ ├── IArchitectureLifecycle.cs -│ ├── IArchitectureModule.cs -│ ├── IArchitecturePhaseAware.cs -│ ├── IArchitectureServices.cs -│ └── IAsyncInitializable.cs -├── command/ # 命令模式接口 -│ ├── ICommand.cs -│ ├── ICommandBus.cs -│ └── ICommandInput.cs -├── controller/ # 控制器接口 -│ └── IController.cs -├── enums/ # 枚举定义 -│ └── ArchitecturePhase.cs -├── environment/ # 环境接口 -│ └── IEnvironment.cs -├── events/ # 事件系统接口 -│ ├── IEasyEvent.cs -│ ├── ITypeEventSystem.cs -│ ├── IUnRegister.cs -│ └── IUnRegisterList.cs -├── ioc/ # 依赖注入容器接口 -│ └── IIocContainer.cs -├── logging/ # 日志系统接口 -│ ├── ILogger.cs -│ ├── ILoggerFactory.cs -│ ├── ILoggerFactoryProvider.cs -│ └── LogLevel.cs -├── model/ # 模型接口 -│ └── IModel.cs -├── properties/ # 配置类 -│ ├── ArchitectureProperties.cs -│ └── LoggerProperties.cs -├── property/ # 可绑定属性接口 -│ ├── IBindableProperty.cs -│ └── IReadonlyBindableProperty.cs -├── query/ # 查询模式接口 -│ ├── IQuery.cs -│ ├── IQueryBus.cs -│ └── IQueryInput.cs -├── rule/ # 规则接口 -│ ├── IContextAware.cs -│ └── ILogAware.cs -├── system/ # 系统接口 -│ └── ISystem.cs -└── utility/ # 工具接口 - ├── IContextUtility.cs - └── IUtility.cs -``` - -## 模块说明 - -### 1. architecture(架构核心) - -架构模块是整个框架的核心,定义了应用架构的生命周期管理和组件注册机制。该模块包含以下接口: - -#### [`IArchitecture`](architecture/IArchitecture.cs) - -架构接口是整个应用的核心管理器,负责管理系统、模型和工具类的注册与获取。它继承自 [ -`IAsyncInitializable`](architecture/IAsyncInitializable.cs) 接口,支持异步初始化。该接口提供了注册系统([ -`RegisterSystem`](architecture/IArchitecture.cs#L39))、注册模型([ -`RegisterModel`](architecture/IArchitecture.cs#L46))、注册工具([ -`RegisterUtility`](architecture/IArchitecture.cs#L53))的方法,以及安装模块([ -`InstallModule`](architecture/IArchitecture.cs#L59))和注册生命周期钩子([ -`RegisterLifecycleHook`](architecture/IArchitecture.cs#L65))的功能。 - -#### [`IArchitectureContext`](architecture/IArchitectureContext.cs) - -架构上下文接口提供了对已注册组件的访问能力,是组件之间通信的桥梁。通过该接口,可以获取系统([ -`GetSystem`](architecture/IArchitectureContext.cs#L22))、模型([ -`GetModel`](architecture/IArchitectureContext.cs#L29))和工具([ -`GetUtility`](architecture/IArchitectureContext.cs#L36))实例。同时,该接口还支持发送命令([ -`SendCommand`](architecture/IArchitectureContext.cs#L42))、发送查询([ -`SendQuery`](architecture/IArchitectureContext.cs#L58))和发送事件([ -`SendEvent`](architecture/IArchitectureContext.cs#L64))等横切操作。 - -#### [`IArchitectureConfiguration`](architecture/IArchitectureConfiguration.cs) - -架构配置接口定义了框架的配置选项,包括日志配置([`LoggerProperties`](architecture/IArchitectureConfiguration.cs#L13) -)和架构配置([`ArchitectureProperties`](architecture/IArchitectureConfiguration.cs#L18) -)。通过该接口,可以在运行时调整框架的行为,如设置日志级别、启用延迟注册等。 - -#### [`IArchitectureLifecycle`](architecture/IArchitectureLifecycle.cs) - -架构生命周期接口定义了架构在不同阶段的回调方法。当架构进入特定阶段时,会通知所有注册的生命周期监听器。该接口主要用于模块化架构的阶段感知。 - -#### [`IArchitectureModule`](architecture/IArchitectureModule.cs) - -架构模块接口继承自 [`IArchitectureLifecycle`](architecture/IArchitectureLifecycle.cs) 和 [ -`IArchitecturePhaseAware`](architecture/IArchitecturePhaseAware.cs) 接口,定义了模块安装到架构的标准方法([ -`Install`](architecture/IArchitectureModule.cs#L13))。通过模块化机制,可以将复杂功能封装为可插拔的模块。 - -#### [`IArchitecturePhaseAware`](architecture/IArchitecturePhaseAware.cs) - -架构阶段感知接口允许组件在架构的不同阶段执行相应的逻辑。该接口提供了 [ -`OnArchitecturePhase`](architecture/IArchitecturePhaseAware.cs#L14) 方法,当架构进入指定阶段时会被调用。 - -#### [`IArchitectureServices`](architecture/IArchitectureServices.cs) - -架构服务接口定义了框架核心服务组件,继承自 [`IContextAware`](rule/IContextAware.cs) 接口。该接口提供了依赖注入容器([ -`Container`](architecture/IArchitectureServices.cs#L18))、类型事件系统([ -`TypeEventSystem`](architecture/IArchitectureServices.cs#L24))、命令总线([ -`CommandBus`](architecture/IArchitectureServices.cs#L29))和查询总线([ -`QueryBus`](architecture/IArchitectureServices.cs#L34))的访问能力。 - -#### [`IAsyncInitializable`](architecture/IAsyncInitializable.cs) - -异步初始化接口定义了组件的异步初始化方法([`InitializeAsync`](architecture/IAsyncInitializable.cs#L14) -)。该接口用于需要执行异步初始化操作的组件,如加载资源、建立网络连接等。 - -### 2. command(命令模式) - -命令模块实现了命令查询职责分离模式(CQRS)中的命令部分,用于封装写操作。该模块包含以下接口: - -#### [`ICommand`](command/ICommand.cs) - -命令接口定义了无返回值命令的基本契约,继承自 [`IContextAware`](rule/IContextAware.cs) 接口。该接口提供了命令执行方法([ -`Execute`](command/ICommand.cs#L15)),用于执行具体的业务逻辑。带返回值的命令由泛型接口 [ -`ICommand`](command/ICommand.cs#L23) 定义,同样继承自 [`IContextAware`](rule/IContextAware.cs) 接口。 - -#### [`ICommandBus`](command/ICommandBus.cs) - -命令总线接口负责命令的发送和执行调度。该接口提供了发送无返回值命令([`Send`](command/ICommandBus.cs#L12) -)和发送带返回值命令([`Send`](command/ICommandBus.cs#L20))的方法。 - -#### [`ICommandInput`](command/ICommandInput.cs) - -命令输入接口是命令模式中输入数据的标记接口,不包含任何成员定义。该接口用于规范化命令的输入参数类型。 - -### 3. controller(控制器) - -控制器模块定义了表现层与业务逻辑层之间的桥梁接口。 - -#### [`IController`](controller/IController.cs) - -控制器接口是 MVC 架构中控制层的抽象定义。该接口作为标记接口使用,不包含任何方法定义,但实现该接口的类通常会获得访问架构上下文的能力,从而可以发送命令、查询数据、注册事件等。 - -### 4. enums(枚举定义) - -枚举模块定义了框架中使用的枚举类型。 - -#### [`ArchitecturePhase`](enums/ArchitecturePhase.cs) - -架构阶段枚举定义了系统架构初始化和运行过程中的各个关键阶段。按照初始化流程,依次包括:`None`(无效阶段)、`BeforeUtilityInit` -(工具类初始化之前)、`AfterUtilityInit`(工具类初始化之后)、`BeforeModelInit`(模型初始化之前)、`AfterModelInit`(模型初始化之后)、 -`BeforeSystemInit`(系统初始化之前)、`AfterSystemInit`(系统初始化之后)、`Ready`(就绪阶段),以及销毁相关阶段:`Destroying` -(正在销毁中)、`Destroyed`(已销毁)、`FailedInitialization`(初始化失败)。 - -### 5. environment(环境接口) - -环境模块定义了应用程序运行环境的抽象接口。 - -#### [`IEnvironment`](environment/IEnvironment.cs) - -环境接口提供了获取应用程序运行环境相关信息的能力。该接口支持根据键值获取配置值([ -`Get`](environment/IEnvironment.cs#L20))、尝试获取环境值([`TryGet`](environment/IEnvironment.cs#L29) -)、获取必需的环境值([`GetRequired`](environment/IEnvironment.cs#L37)),以及注册键值对([ -`Register`](environment/IEnvironment.cs#L44))和初始化环境([`Initialize`](environment/IEnvironment.cs#L49))。 - -### 6. events(事件系统) - -事件模块实现了框架的事件驱动通信机制,支持类型安全和松耦合的组件通信。 - -#### [`ITypeEventSystem`](events/ITypeEventSystem.cs) - -类型事件系统接口是基于类型的事件发布-订阅机制的抽象定义。该接口支持发送无参事件([ -`Send`](events/ITypeEventSystem.cs#L14))、发送带参事件([`Send`](events/ITypeEventSystem.cs#L21))、注册事件监听器([ -`Register`](events/ITypeEventSystem.cs#L29))和注销事件监听器([`UnRegister`](events/ITypeEventSystem.cs#L36))。 - -#### [`IEasyEvent`](events/IEasyEvent.cs) - -简单事件接口定义了基础的事件注册功能([`Register`](events/IEasyEvent.cs#L15))。该接口用于简单的无参事件场景。 - -#### [`IUnRegister`](events/IUnRegister.cs) - -注销接口提供了事件监听器的注销功能([`UnRegister`](events/IUnRegister.cs#L11))。所有事件注册方法的返回值都实现了该接口,用于在适当时机取消事件监听。 - -#### [`IUnRegisterList`](events/IUnRegisterList.cs) - -统一注销接口提供了管理多个注销句柄的能力。该接口维护了一个注销对象列表([`UnregisterList`](events/IUnRegisterList.cs#L13) -),可以在批量注销时统一处理。 - -### 7. ioc(依赖注入) - -IoC 模块实现了控制反转和依赖注入机制,用于管理组件的生命周期和依赖关系。 - -#### [`IIocContainer`](ioc/IIocContainer.cs) - -依赖注入容器接口是框架的核心服务接口之一,继承自 [`IContextAware`](rule/IContextAware.cs) -接口。该接口提供了丰富的服务注册和解析方法,包括注册单例([`RegisterSingleton`](ioc/IIocContainer.cs#L22) -)、注册多个实例([`RegisterPlurality`](ioc/IIocContainer.cs#L30))、注册系统([`RegisterSystem`](ioc/IIocContainer.cs#L36) -)、注册类型([`Register`](ioc/IIocContainer.cs#L43))等。在解析方面,支持获取单个实例([`Get`](ioc/IIocContainer.cs#L62) -)、获取必需实例([`GetRequired`](ioc/IIocContainer.cs#L70))、获取所有实例([`GetAll`](ioc/IIocContainer.cs#L77) -)和获取排序后的实例([`GetAllSorted`](ioc/IIocContainer.cs#L85))。此外,还提供了检查([ -`Contains`](ioc/IIocContainer.cs#L96))、清空([`Clear`](ioc/IIocContainer.cs#L108))和冻结([ -`Freeze`](ioc/IIocContainer.cs#L113))等实用方法。 - -### 8. logging(日志系统) - -日志模块提供了完整的日志记录抽象,支持多级别日志输出。 - -#### [`LogLevel`](logging/LogLevel.cs) - -日志级别枚举定义了日志消息的严重程度等级,包括:`Trace`(跟踪级别,用于详细的程序执行流程信息)、`Debug`(调试级别,用于调试过程中的详细信息)、 -`Info`(信息级别,用于一般性的程序运行信息)、`Warning`(警告级别,用于表示可能的问题或异常情况)、`Error` -(错误级别,用于表示错误但程序仍可继续运行的情况)、`Fatal`(致命级别,用于表示严重的错误导致程序无法继续运行)。 - -#### [`ILogger`](logging/ILogger.cs) - -日志记录器接口是框架日志系统的核心接口,提供了完整的日志记录能力。该接口支持以下功能: - -- **级别启用检查**:提供了各个日志级别的是否启用检查方法,如 [`IsTraceEnabled`](logging/ILogger.cs#L22)、[ - `IsDebugEnabled`](logging/ILogger.cs#L28)、[`IsInfoEnabled`](logging/ILogger.cs#L34)、[ - `IsWarnEnabled`](logging/ILogger.cs#L40)、[`IsErrorEnabled`](logging/ILogger.cs#L46)、[ - `IsFatalEnabled`](logging/ILogger.cs#L52),以及通用的 [`IsEnabledForLevel`](logging/ILogger.cs#L59) 方法。 -- **日志记录方法**:每个日志级别都有多种重载形式,支持简单消息、格式化消息和异常记录。格式化为 `Trace`、`Debug`、`Info`、 - `Warn`、`Error`、`Fatal` 六个级别,每个级别都提供了 `msg`、`format+arg`、`format+arg1+arg2`、`format+params`、`msg+exception` - 等多种调用方式。 -- **获取日志记录器名称**:通过 [`Name`](logging/ILogger.cs#L14) 方法获取日志记录器的名称。 - -#### [`ILoggerFactory`](logging/ILoggerFactory.cs) - -日志工厂接口用于创建日志记录器实例([`GetLogger`](logging/ILoggerFactory.cs#L14))。该接口支持指定日志记录器名称和最小日志级别。 - -#### [`ILoggerFactoryProvider`](logging/ILoggerFactoryProvider.cs) - -日志工厂提供者接口扩展了日志工厂的功能,支持动态设置最小日志级别([`MinLevel`](logging/ILoggerFactoryProvider.cs#L11) -)和创建日志记录器([`CreateLogger`](logging/ILoggerFactoryProvider.cs#L18))。 - -### 9. model(模型接口) - -模型模块定义了数据层的抽象接口。 - -#### [`IModel`](model/IModel.cs) - -模型接口继承自 [`IContextAware`](rule/IContextAware.cs) 和 [ -`IArchitecturePhaseAware`](architecture/IArchitecturePhaseAware.cs) -接口,定义了模型组件的基本行为。该接口提供了模型初始化方法([`Init`](model/IModel.cs#L14)),用于执行模型相关的初始化逻辑。 - -### 10. properties(配置类) - -配置模块定义了框架使用的配置选项类。 - -#### [`LoggerProperties`](properties/LoggerProperties.cs) - -日志配置选项类用于配置日志系统的相关参数,包含日志工厂提供程序属性([ -`LoggerFactoryProvider`](properties/LoggerProperties.cs#L14))。 - -#### [`ArchitectureProperties`](properties/ArchitectureProperties.cs) - -架构选项配置类用于定义架构行为的相关配置选项,包含两个属性:`AllowLateRegistration`(允许延迟注册开关,控制是否允许在初始化完成后进行组件注册)和 -`StrictPhaseValidation`(严格阶段验证开关,控制是否启用严格的阶段验证机制)。 - -### 11. property(可绑定属性) - -可绑定属性模块实现了响应式数据绑定机制,支持数据变化的自动通知。 - -#### [`IReadonlyBindableProperty`](property/IReadonlyBindableProperty.cs) - -只读可绑定属性接口继承自 [`IEasyEvent`](events/IEasyEvent.cs) -接口,提供了属性值的读取和变更监听功能。该接口支持获取当前值([`Value`](property/IReadonlyBindableProperty.cs#L15) -)、注册带初始值的回调([`RegisterWithInitValue`](property/IReadonlyBindableProperty.cs#L22))、取消注册回调([ -`UnRegister`](property/IReadonlyBindableProperty.cs#L28))和注册回调([ -`Register`](property/IReadonlyBindableProperty.cs#L35))。 - -#### [`IBindableProperty`](property/IBindableProperty.cs) - -可绑定属性接口继承自只读可绑定属性接口,提供了可读写的属性绑定功能。该接口在只读接口的基础上增加了属性值的设置能力([ -`Value`](property/IBindableProperty.cs#L12) 的 setter)以及不触发事件的设置方法([ -`SetValueWithoutEvent`](property/IBindableProperty.cs#L18))。 - -### 12. query(查询模式) - -查询模块实现了命令查询职责分离模式(CQRS)中的查询部分,用于封装读操作。 - -#### [`IQuery`](query/IQuery.cs) - -查询接口继承自 [`IContextAware`](rule/IContextAware.cs) 接口,定义了执行查询操作的契约。该接口提供了查询执行方法([ -`Do`](query/IQuery.cs#L15)),返回指定类型的结果。 - -#### [`IQueryBus`](query/IQueryBus.cs) - -查询总线接口负责查询的发送和执行调度。该接口提供了发送查询并返回结果的方法([`Send`](query/IQueryBus.cs#L14))。 - -#### [`IQueryInput`](query/IQueryInput.cs) - -查询输入接口是查询模式中输入数据的标记接口,不包含任何成员定义。 - -### 13. rule(规则接口) - -规则模块定义了框架组件需要遵循的约束和规则接口。 - -#### [`IContextAware`](rule/IContextAware.cs) - -上下文感知接口允许实现类设置和获取架构上下文。该接口提供了设置上下文([`SetContext`](rule/IContextAware.cs#L14) -)和获取上下文([`GetContext`](rule/IContextAware.cs#L20))的方法。框架中大多数核心组件(如命令、查询、系统、模型、工具、IoC -容器等)都实现了该接口,以获得访问架构服务的能力。 - -#### [`ILogAware`](rule/ILogAware.cs) - -日志感知接口允许实现类设置和使用日志记录器。该接口提供了设置日志记录器的方法([`SetLogger`](rule/ILogAware.cs#L14))。 - -### 14. system(系统接口) - -系统模块定义了业务逻辑层的抽象接口。 - -#### [`ISystem`](system/ISystem.cs) - -系统接口继承自 [`IContextAware`](rule/IContextAware.cs) 和 [ -`IArchitecturePhaseAware`](architecture/IArchitecturePhaseAware.cs) -接口,定义了系统组件的基本行为。该接口提供了系统初始化方法([`Init`](system/ISystem.cs#L16))和系统销毁方法([ -`Destroy`](system/ISystem.cs#L22)),用于管理系统的生命周期。 - -### 15. utility(工具接口) - -工具模块定义了无状态工具类的抽象接口。 - -#### [`IUtility`](utility/IUtility.cs) - -工具接口是所有工具类实现的基础接口,作为标记接口使用,不包含任何成员定义。该接口定义了通用工具类的基本契约。 - -#### [`IContextUtility`](utility/IContextUtility.cs) - -上下文工具接口继承自 [`IUtility`](utility/IUtility.cs) 和 [`IContextAware`](rule/IContextAware.cs) -接口,提供了具有上下文感知能力的工具功能。该接口在工具接口的基础上增加了初始化方法([ -`Init`](utility/IContextUtility.cs#L14))。 - -## 接口继承关系图 - -``` -IArchitecture - └── IAsyncInitializable - -IArchitectureModule - ├── IArchitectureLifecycle - └── IArchitecturePhaseAware - -IArchitectureServices - └── IContextAware - -ICommand - └── IContextAware - -ICommand - └── IContextAware - -IIocContainer - └── IContextAware - -IQuery - └── IContextAware - -IModel - ├── IContextAware - └── IArchitecturePhaseAware - -ISystem - ├── IContextAware - └── IArchitecturePhaseAware - -IContextUtility - ├── IUtility - └── IContextAware - -IReadonlyBindableProperty - └── IEasyEvent - -IBindableProperty - └── IReadonlyBindableProperty -``` - -## 核心能力接口 - -框架中的组件通过实现特定的接口来获得相应的能力。这些能力接口主要分为以下几类: - -### 上下文感知能力 - -通过实现 [`IContextAware`](rule/IContextAware.cs) 接口,组件可以获得设置和获取架构上下文的能力,从而访问框架提供的各种服务。命令、查询、系统、模型、工具、IoC -容器等核心组件都实现了该接口。 - -### 日志能力 - -通过实现 [`ILogAware`](rule/ILogAware.cs) 接口,组件可以获得使用日志记录器的能力。该接口提供了设置日志记录器的方法,使组件可以输出日志信息。 - -### 阶段感知能力 - -通过实现 [`IArchitecturePhaseAware`](architecture/IArchitecturePhaseAware.cs) -接口,组件可以在架构的不同阶段执行相应的逻辑。该接口提供了 [ -`OnArchitecturePhase`](architecture/IArchitecturePhaseAware.cs#L14) 方法,当架构进入指定阶段时会被调用。 - -## 使用指南 - -### 实现框架组件 - -当需要实现框架的组件时,通常需要实现相应的接口并遵循框架的生命周期约定: - -```csharp -// 实现系统组件 -public class CombatSystem : ISystem -{ - public void Init() - { - // 系统初始化逻辑 - } - - public void Destroy() - { - // 系统销毁逻辑 - } -} - -// 实现模型组件 -public class PlayerModel : IModel -{ - public void Init() - { - // 模型初始化逻辑 - } -} - -// 实现工具组件 -public class StorageUtility : IUtility -{ - // 工具类方法 -} - -// 实现命令 -public class AttackCommand : ICommand -{ - public void Execute() - { - // 命令执行逻辑 - } -} - -// 实现查询 -public class GetPlayerInfoQuery : IQuery -{ - public PlayerInfo Do() - { - // 查询执行逻辑 - } -} -``` - -### 访问框架服务 - -通过实现 [`IContextAware`](rule/IContextAware.cs) 接口,组件可以获得访问框架服务的能力: - -```csharp -public class MySystem : ISystem -{ - private IArchitectureContext _context; - - public void SetContext(IArchitectureContext context) - { - _context = context; - } - - public IArchitectureContext GetContext() - { - return _context; - } - - private void SomeMethod() - { - // 获取其他组件 - var playerModel = _context.GetModel(); - - // 发送命令 - _context.SendCommand(new AttackCommand()); - - // 发送查询 - var health = _context.SendQuery(new GetHealthQuery()); - - // 发送事件 - _context.SendEvent(new GameEvent()); - - // 注册事件监听 - _context.RegisterEvent(OnEnemySpawned); - } -} -``` - -## 相关文档 - -- [GFramework.Core](../GFramework.Core/README.md) - 核心框架实现模块 -- [GFramework.Godot](../GFramework.Godot/README.md) - Godot 平台集成模块 -- [架构模块文档](architecture/README.md) - 架构接口详细说明 -- [控制器模块文档](controller/README.md) - 控制器使用说明 - ---- - -**版本**: 1.0.0 - -**许可证**: Apache 2.0 +参见 [docs/core/](../docs/core/) 目录下的详细文档。 diff --git a/GFramework.Core.Tests/TEST_COVERAGE_PLAN.md b/GFramework.Core.Tests/TEST_COVERAGE_PLAN.md deleted file mode 100644 index fb6d5903..00000000 --- a/GFramework.Core.Tests/TEST_COVERAGE_PLAN.md +++ /dev/null @@ -1,581 +0,0 @@ -# GFramework.Core 模块测试覆盖详细清单 - -> **生成日期**: 2026-01-18 -> **最后更新**: 2026-01-18 -> **当前版本**: Core测试覆盖率 ~79.2% (文件级别) -> **目标**: 提升Core模块测试覆盖率至 95%+ 并补充缺失的单元测试 - ---- - -## 📊 总体统计 - -| 类别 | 源文件数 | 有测试文件数 | 缺失测试文件数 | 测试覆盖率 | -|--------|--------|--------|---------|-----------| -| 架构系统 | 6 | 4 | 1 | 83% | -| 事件系统 | 8 | 5 | 0 | 100% | -| 命令系统 | 4 | 1 | 3 | 25% | -| 查询系统 | 5 | 1 | 3 | 20% | -| 日志系统 | 5 | 2 | 0 | 100% | -| 扩展方法 | 4 | 2 | 0 | 100% | -| 状态系统 | 4 | 2 | 0 | 100% | -| IOC容器 | 1 | 1 | 0 | 100% | -| 模型系统 | 1 | 0 | 0 | 100% | -| 系统基类 | 1 | 0 | 0 | 100% | -| 对象池 | 1 | 1 | 0 | 100% | -| 属性系统 | 2 | 1 | 0 | 100% | -| 规则系统 | 1 | 0 | 0 | 100% | -| 工具类 | 1 | 0 | 1 | 0% | -| 环境系统 | 2 | 1 | 0 | 100% | -| 常量 | 2 | 0 | 2 | 0% | -| 协程系统 | 11 | 4 | 0 | 100% | -| **总计** | **59** | **24** | **10** | **83.1%** | - -> **注**: 标记为0个测试文件的模块通过间接测试(集成测试)实现了功能覆盖 -> **重要发现**: 命令系统和查询系统的异步功能完全缺失测试! - ---- - -## 🎯 测试补充优先级概览 - -| 优先级 | 任务数 | 预计测试数 | 描述 | -|---------|-------|-------------|-------------| -| 🔴 高优先级 | 5 | 34-44 | 异步核心功能和工具基类 | -| 🟡 中优先级 | 2 | 6-10 | 常量验证测试 | -| ✅ 已完成 | 1 | 61 | 协程系统完整测试 | -| **总计** | **7** | **101-115** | - | - ---- - -## 📋 详细源文件与测试文件对应关系 - -### Architecture 模块 (6个源文件) - -| 源文件 | 对应测试文件 | 测试覆盖 | -|------------------------------|-----------------------------------------------------|---------| -| Architecture.cs | SyncArchitectureTests.cs, AsyncArchitectureTests.cs | ✅ 98个测试 | -| ArchitectureConfiguration.cs | ArchitectureConfigurationTests.cs | ✅ 12个测试 | -| ArchitectureConstants.cs | **缺失** | ❌ 需补充 | -| ArchitectureContext.cs | ArchitectureContextTests.cs | ✅ 22个测试 | -| ArchitectureServices.cs | ArchitectureServicesTests.cs | ✅ 15个测试 | -| GameContext.cs | GameContextTests.cs | ✅ 已有测试 | - -**测试用例总数**: 147个 - ---- - -### Command 模块 (4个源文件) - -| 源文件 | 对应测试文件 | 测试覆盖 | -|-----------------------------|-------------------------|------------| -| **AbstractAsyncCommand.cs** | **缺失** | ❌ 需创建测试文件 | -| AbstractCommand.cs | CommandBusTests.cs (间接) | ✅ 已覆盖 | -| **CommandBus.cs** | CommandBusTests.cs | ⚠️ 需补充异步测试 | -| EmptyCommandInput.cs | CommandBusTests.cs (间接) | ✅ 已覆盖 | - -**测试用例总数**: 4个(需补充异步测试) - -**需要补充**: - -1. ❌ AbstractAsyncCommandTests.cs - 新建(高优先级) -2. ❌ CommandBusTests.cs - 补充 SendAsync 方法测试(高优先级) - ---- - -### Query 模块 (5个源文件) - -| 源文件 | 对应测试文件 | 测试覆盖 | -|---------------------------|-----------------------|-----------| -| **AbstractAsyncQuery.cs** | **缺失** | ❌ 需创建测试文件 | -| AbstractQuery.cs | QueryBusTests.cs (间接) | ✅ 已覆盖 | -| **AsyncQueryBus.cs** | **缺失** | ❌ 需创建测试文件 | -| EmptyQueryInput.cs | QueryBusTests.cs (间接) | ✅ 已覆盖 | -| QueryBus.cs | QueryBusTests.cs | ✅ 3个测试 | - -**测试用例总数**: 3个(需补充异步测试) - -**需要补充**: - -1. ❌ AbstractAsyncQueryTests.cs - 新建(高优先级) -2. ❌ AsyncQueryBusTests.cs - 新建(高优先级) - ---- - -### Constants 模块 (2个源文件) - -| 源文件 | 对应测试文件 | 测试覆盖 | -|--------------------------|--------|-------| -| ArchitectureConstants.cs | **缺失** | ❌ 需补充 | -| GFrameworkConstants.cs | **缺失** | ❌ 需补充 | - -**测试用例总数**: 0个 - -**需要补充**: - -1. ❌ ArchitectureConstantsTests.cs - 新建(中优先级) -2. ❌ GFrameworkConstantsTests.cs - 新建(中优先级) - ---- - -### Utility 模块 (1个源文件) - -| 源文件 | 对应测试文件 | 测试覆盖 | -|---------------------------|--------|-----------| -| AbstractContextUtility.cs | **缺失** | ❌ 需创建测试文件 | - -**测试用例总数**: 0个 - -**需要补充**: - -1. ❌ AbstractContextUtilityTests.cs - 新建(高优先级) - ---- - -### 协程系统 模块 (11个源文件) - -| 源文件 | 对应测试文件 | 测试覆盖 | -|-----------------------|---------------------------------|---------| -| CoroutineState.cs | CoroutineStateTests.cs | ✅ 2个测试 | -| ITimeSource.cs | CoroutineSchedulerTests.cs (间接) | ✅ 已覆盖 | -| IYieldInstruction.cs | YieldInstructionTests.cs (间接) | ✅ 已覆盖 | -| CoroutineHandle.cs | CoroutineHandleTests.cs | ✅ 15个测试 | -| CoroutineHelper.cs | CoroutineHelperTests.cs | ✅ 19个测试 | -| CoroutineMetadata.cs | CoroutineSchedulerTests.cs (间接) | ✅ 已覆盖 | -| CoroutineScheduler.cs | CoroutineSchedulerTests.cs | ✅ 25个测试 | -| CoroutineSlot.cs | CoroutineSchedulerTests.cs (间接) | ✅ 已覆盖 | -| Delay.cs | YieldInstructionTests.cs (间接) | ✅ 已覆盖 | -| WaitForCoroutine.cs | YieldInstructionTests.cs (间接) | ✅ 已覆盖 | -| WaitForFrames.cs | YieldInstructionTests.cs (间接) | ✅ 已覆盖 | -| WaitOneFrame.cs | YieldInstructionTests.cs (间接) | ✅ 已覆盖 | -| WaitUntil.cs | YieldInstructionTests.cs (间接) | ✅ 已覆盖 | -| WaitWhile.cs | YieldInstructionTests.cs (间接) | ✅ 已覆盖 | - -**测试用例总数**: 61个 - -**需要补充**: - -无需补充,协程系统测试覆盖率已达 100% - ---- - -### 其他模块 (Events, Logging, IoC, etc.) - -所有其他模块的测试覆盖率均达到 100%(包括间接测试覆盖),详见下文详细列表。 - ---- - -## 🔴 高优先级 - 异步核心功能(5个任务) - -### 任务1: CommandBusTests.cs - 补充异步测试 - -**源文件路径**: `GFramework.Core/command/CommandBus.cs` - -**优先级**: 🔴 高 - -**原因**: CommandBus 已实现 SendAsync 方法但没有任何测试 - -**需要补充的测试内容**: - -- ✅ SendAsync(IAsyncCommand) 方法 - 执行无返回值的异步命令 -- ✅ SendAsync(IAsyncCommand) 方法 - 处理 null 异步命令 -- ✅ SendAsync(IAsyncCommand) 方法 - 执行有返回值的异步命令 -- ✅ SendAsync(IAsyncCommand) 方法 - 处理 null 异步命令 - -**预计测试数**: 4 个 - -**测试文件**: `GFramework.Core.Tests/command/CommandBusTests.cs` - -**操作**: 在现有测试文件中补充异步测试方法 - -**状态**: ❌ 待补充 - ---- - -### 任务2: AbstractAsyncCommandTests.cs - -**源文件路径**: `GFramework.Core/command/AbstractAsyncCommand.cs` - -**优先级**: 🔴 高 - -**原因**: 异步命令基类没有任何单元测试,是核心功能 - -**需要测试的内容**: - -- ✅ 异步命令无返回值版本的基础实现 -- ✅ 异步命令有返回值版本的基础实现 -- ✅ ExecuteAsync 方法调用 -- ✅ ExecuteAsync 方法的异常处理 -- ✅ 上下文感知功能(SetContext, GetContext) -- ✅ 日志功能(Logger属性) -- ✅ 子类继承行为验证(两个版本) -- ✅ 命令执行前日志记录 -- ✅ 命令执行后日志记录 -- ✅ 错误情况下的日志记录 - -**预计测试数**: 10-12 个 - -**创建路径**: `GFramework.Core.Tests/command/AbstractAsyncCommandTests.cs` - -**状态**: ❌ 待创建 - ---- - -### 任务3: AsyncQueryBusTests.cs - -**源文件路径**: `GFramework.Core/query/AsyncQueryBus.cs` - -**优先级**: 🔴 高 - -**原因**: 异步查询总线是核心组件,需要完整的单元测试 - -**需要测试的内容**: - -- ✅ SendAsync 方法 - 正常查询发送 -- ✅ SendAsync 方法 - 空查询异常 -- ✅ 异步查询结果正确性 -- ✅ 不同返回类型的异步查询支持 -- ✅ 异步查询的异常处理 -- ✅ 异步查询的上下文传递 - -**预计测试数**: 6-8 个 - -**创建路径**: `GFramework.Core.Tests/query/AsyncQueryBusTests.cs` - -**状态**: ❌ 待创建 - ---- - -### 任务4: AbstractAsyncQueryTests.cs - -**源文件路径**: `GFramework.Core/query/AbstractAsyncQuery.cs` - -**优先级**: 🔴 高 - -**原因**: 异步查询基类没有任何单元测试,是核心功能 - -**需要测试的内容**: - -- ✅ 异步查询的基础实现 -- ✅ DoAsync 方法调用 -- ✅ DoAsync 方法的异常处理 -- ✅ 上下文感知功能(SetContext, GetContext) -- ✅ 日志功能(Logger属性) -- ✅ 子类继承行为验证 -- ✅ 查询执行前日志记录 -- ✅ 查询执行后日志记录 -- ✅ 返回值类型验证 -- ✅ 错误情况下的日志记录 - -**预计测试数**: 8-10 个 - -**创建路径**: `GFramework.Core.Tests/query/AbstractAsyncQueryTests.cs` - -**状态**: ❌ 待创建 - ---- - -### 任务5: AbstractContextUtilityTests.cs - -**源文件路径**: `GFramework.Core/utility/AbstractContextUtility.cs` - -**优先级**: 🔴 高 - -**原因**: 工具基类需要直接的单元测试以确保其基础功能正确性 - -**需要测试的内容**: - -- ✅ 抽象工具类实现 -- ✅ IContextUtility 接口实现 -- ✅ Init 方法调用 -- ✅ 日志初始化 -- ✅ 上下文感知功能(SetContext, GetContext) -- ✅ 子类继承行为 -- ✅ 工具初始化日志记录 -- ✅ 工具生命周期完整性 - -**预计测试数**: 6-8 个 - -**创建路径**: `GFramework.Core.Tests/utility/AbstractContextUtilityTests.cs` - -**状态**: ❌ 待创建 - ---- - -## 🟡 中优先级 - 常量验证(2个任务) - -### 任务6: ArchitectureConstantsTests.cs - -**源文件路径**: `GFramework.Core/architecture/ArchitectureConstants.cs` - -**优先级**: 🟡 中 - -**原因**: 验证架构相关的常量定义是否正确 - -**需要测试的内容**: - -- ✅ 常量值的正确性 -- ✅ 常量类型验证 -- ✅ 常量可访问性 -- ✅ 常量命名规范 -- ✅ 架构阶段定义常量 - -**预计测试数**: 3-5 个 - -**创建路径**: `GFramework.Core.Tests/architecture/ArchitectureConstantsTests.cs` - -**状态**: ❌ 待创建 - ---- - -### 任务7: GFrameworkConstantsTests.cs - -**源文件路径**: `GFramework.Core/constants/GFrameworkConstants.cs` - -**优先级**: 🟡 中 - -**原因**: 验证框架级别的常量定义 - -**需要测试的内容**: - -- ✅ 版本号常量格式正确性 -- ✅ 其他框架常量 -- ✅ 常量值正确性 -- ✅ 常量类型验证 -- ✅ 常量可访问性 - -**预计测试数**: 3-5 个 - -**创建路径**: `GFramework.Core.Tests/constants/GFrameworkConstantsTests.cs` - -**状态**: ❌ 待创建 - ---- - -## 📊 测试执行计划 - -### 第一批:异步核心功能(4个任务,预计 1.5小时) - -| 序号 | 测试任务 | 操作 | 预计测试数 | 优先级 | 预计时间 | -|----|------------------------------|----|-------|------|------| -| 1 | CommandBusTests.cs - 补充异步测试 | 补充 | 4 | 🔴 高 | 20分钟 | -| 2 | AbstractAsyncCommandTests.cs | 新建 | 10-12 | 🔴 高 | 30分钟 | -| 3 | AsyncQueryBusTests.cs | 新建 | 6-8 | 🔴 高 | 25分钟 | -| 4 | AbstractAsyncQueryTests.cs | 新建 | 8-10 | 🔴 高 | 25分钟 | - -**小计**: 28-34 个测试,约 1.5小时 - ---- - -### 第二批:工具基类(1个任务,预计 15分钟) - -| 序号 | 测试文件 | 操作 | 预计测试数 | 优先级 | 预计时间 | -|----|--------------------------------|----|-------|------|------| -| 5 | AbstractContextUtilityTests.cs | 新建 | 6-8 | 🔴 高 | 15分钟 | - -**小计**: 6-8 个测试 - ---- - -### 第三批:常量验证(2个任务,预计 20分钟) - -| 序号 | 测试文件 | 操作 | 预计测试数 | 优先级 | 预计时间 | -|----|-------------------------------|----|-------|------|------| -| 6 | ArchitectureConstantsTests.cs | 新建 | 3-5 | 🟡 中 | 10分钟 | -| 7 | GFrameworkConstantsTests.cs | 新建 | 3-5 | 🟡 中 | 10分钟 | - -**小计**: 6-10 个测试 - ---- - -## 📊 最终统计 - -| 批次 | 任务数 | 操作 | 预计测试数 | 预计时间 | -|----------|-------|-------------|-----------|---------| -| 第一批(异步) | 4 | 3新建+1补充 | 28-34 | 1.5小时 | -| 第二批(高优先) | 1 | 新建 | 6-8 | 15分钟 | -| 第三批(中优先) | 2 | 新建 | 6-10 | 20分钟 | -| **总计** | **7** | **6新建+1补充** | **40-54** | **2小时** | - ---- - -## 🎯 目标达成路径 - -### 当前状态(2026-01-18) - -- **现有测试数**: 496 个 -- **文件覆盖率**: 79.2% (38/48个文件有测试覆盖) -- **缺失测试**: 40-54 个 -- **已完成文件**: 38/48 -- **关键发现**: 异步命令和查询功能完全缺失测试 - -### 协程模块新增后状态(2026-01-21) - -- **现有测试数**: 496 + 61 = 557 个 -- **文件覆盖率**: 83.1% (49/59个文件有测试覆盖) -- **协程模块测试**: 61 个(已完成) -- **协程模块文件**: 11 个 -- **关键发现**: 协程系统测试覆盖率已达 100% - -### 补充测试完成后预计 - -- **预计测试数**: 557 + 40-54 = 597-611 个 -- **预计文件覆盖率**: ~95% (56/59) -- **代码行覆盖率**: 预计 90%+ (需通过覆盖率工具精确测量) - ---- - -## 📝 注意事项 - -### 注释规范 - -- ✅ 生成的测试类需要有注释说明这个测试类具体有哪些测试 -- ✅ 测试方法需要有注释说明具体测试的是什么 -- ✅ 对于复杂逻辑的测试方法,需要有标准的行注释说明逻辑,不要使用行尾注释 -- ✅ 对于类与方法的测试,需要标准的C#文档注释 - -### 测试隔离性 - -1. ✅ 每个测试文件使用独立的测试辅助类(TestXxxV2, TestXxxV3等) -2. ✅ 避免与现有测试类(TestSystem, TestModel)命名冲突 -3. ✅ 使用 `[SetUp]` 和 `[TearDown]` 确保测试隔离 -4. ✅ 必要时使用 `[NonParallelizable]` 特性 -5. ✅ 异步测试需要正确使用 `async/await` 模式 - -### 测试命名规范 - -- 测试类:`{Component}Tests` -- 测试方法:`{Scenario}_Should_{ExpectedOutcome}` -- 测试辅助类:`Test{Component}V{Version}` -- 异步测试方法建议包含 `Async` 关键字 - -### 构建和验证流程 - -1. 编写测试代码 -2. 运行 `dotnet build` 验证编译 -3. 运行 `dotnet test` 执行测试 -4. 检查测试通过率 -5. 修复失败或隔离性问题 - -### 异步测试最佳实践 - -1. **正确使用 async/await** - - 测试方法标记为 `async Task` - - 所有异步操作使用 `await` - - 不要使用 `.Result` 或 `.Wait()` 导致死锁 - -2. **异常测试** - - 使用 `Assert.ThrowsAsync` 测试异步异常 - - 确保异常在正确的位置抛出 - -3. **测试辅助类** - - 创建模拟的异步命令/查询类 - - 验证异步操作是否正确执行 - - 测试并发场景(如需要) - -### 代码覆盖率工具建议 - -建议添加 Coverlet 代码覆盖率工具以获得精确的覆盖率数据: - -```xml - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - -``` - -运行覆盖率命令: - -```bash -dotnet test --collect:"XPlat Code Coverage" -``` - ---- - -## 🔄 更新日志 - -| 日期 | 操作 | 说明 | -|------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 2026-01-16 | 初始创建 | 生成原始测试覆盖清单(包含错误) | -| 2026-01-18 | 全面更新(第1版) | 重新检查框架和测试,修正以下问题:
1. 删除不存在的ContextAwareStateMachineTests.cs
2. 更新实际测试数量为496个
3. 添加新增源文件
4. 修正文件覆盖率从41%提升至91.5%
5. 调整优先级,从26个减少到3个缺失测试文件 | -| 2026-01-18 | 全面更新(第2版) | 补充异步命令和异步查询测试计划:
1. 发现CommandBus已有SendAsync实现但无测试
2. 发现AbstractAsyncCommand、AsyncQueryBus、AbstractAsyncQuery无测试
3. 新增4个高优先级异步测试任务
4. 更新文件覆盖率从91.5%调整为79.2%(补充异步后)
5. 总测试数从40-54调整为目标 | -| 2026-01-21 | 协程模块测试完成 | 新增协程系统模块测试:
1. 新增协程源文件11个(GFramework.Core和GFramework.Core.Abstractions)
2. 创建协程测试文件4个,测试用例61个
3. 协程系统测试覆盖率达到100%
4. 更新文件覆盖率从79.2%提升至83.1%
5. 总测试数从496增加至557个 | - ---- - -## 📌 待确认事项 - -- [x] 确认优先级划分是否合理 -- [x] 确认执行计划是否可行 -- [x] 确认测试用例数量估算是否准确 -- [x] 确认测试隔离策略是否完整 -- [ ] 添加代码覆盖率工具配置 -- [ ] 确定是否需要补充间接测试为直接测试 - ---- - -## 🎉 成就解锁 - -### 已完成的测试覆盖 - -✅ **架构系统核心功能** - 147个测试覆盖 -✅ **事件系统完整功能** - 37个测试覆盖 -✅ **日志系统完整功能** - 69个测试覆盖 -✅ **IoC容器** - 21个测试覆盖 -✅ **状态机系统** - 33个测试覆盖 -✅ **对象池系统** - 6个测试覆盖 -✅ **属性系统** - 8个测试覆盖 -✅ **扩展方法** - 17个测试覆盖 -✅ **同步命令查询系统** - 通过集成测试覆盖 -✅ **模型系统** - 通过架构集成测试覆盖 -✅ **系统基类** - 通过架构集成测试覆盖 -✅ **协程系统完整功能** - 61个测试覆盖 - -### 待补充的异步功能 - -❌ **异步命令系统** - AbstractAsyncCommand、CommandBus.SendAsync -❌ **异步查询系统** - AsyncQueryBus、AbstractAsyncQuery -❌ **工具基类** - AbstractContextUtility -❌ **常量验证** - ArchitectureConstants、GFrameworkConstants - -### 测试质量指标 - -- **测试用例总数**: 557个 -- **文件级别覆盖率**: 83.1% -- **支持测试的.NET版本**: .NET 8.0, .NET 10.0 -- **测试框架**: NUnit 3.x -- **测试隔离性**: 良好 -- **测试组织结构**: 清晰(按模块分类) - ---- - -## 🚀 实施进度 - -### 协程系统测试(已完成) - -- [x] 协程系统测试模块 (61个测试) - - [x] CoroutineStateTests.cs (2个测试) - - [x] CoroutineHandleTests.cs (15个测试) - - [x] CoroutineHelperTests.cs (19个测试) - - [x] YieldInstructionTests.cs (25个测试) - - [x] CoroutineSchedulerTests.cs (25个测试,包含TestTimeSource辅助类) - -### 第一批:异步核心功能 - -- [ ] 任务1: CommandBusTests.cs - 补充异步测试 (4个测试) -- [ ] 任务2: AbstractAsyncCommandTests.cs (10-12个测试) -- [ ] 任务3: AsyncQueryBusTests.cs (6-8个测试) -- [ ] 任务4: AbstractAsyncQueryTests.cs (8-10个测试) - -### 第二批:工具基类 - -- [ ] 任务5: AbstractContextUtilityTests.cs (6-8个测试) - -### 第三批:常量验证 - -- [ ] 任务6: ArchitectureConstantsTests.cs (3-5个测试) -- [ ] 任务7: GFrameworkConstantsTests.cs (3-5个测试) - ---- - -**文档维护**: 请在完成每个测试任务后更新本文档的状态和实施进度 diff --git a/GFramework.Core/README.md b/GFramework.Core/README.md index c50b8876..c4fb096e 100644 --- a/GFramework.Core/README.md +++ b/GFramework.Core/README.md @@ -1,507 +1,31 @@ -# GFramework.Core 核心框架 +# GFramework.Core -> 一个基于 CQRS、MVC 和事件驱动的轻量级游戏开发架构框架 +GFramework 框架的核心模块,提供MVC架构的基础设施。 -## 目录 +## 主要功能 -- [框架概述](#框架概述) -- [核心概念](#核心概念) -- [架构图](#架构图) -- [快速开始](#快速开始) -- [包说明](#包说明) -- [组件联动](#组件联动) -- [最佳实践](#最佳实践) -- [设计理念](#设计理念) +- **Architecture** - 应用程序架构管理,支持依赖注入、生命周期管理和模块化扩展 +- **Model** - 数据模型层,管理应用状态和数据 +- **System** - 业务逻辑层,处理核心业务逻辑和事件响应 +- **Controller** - 控制器层,处理用户输入和UI协调 +- **Command** - 命令模式实现,封装用户操作 +- **Query** - 查询模式实现,支持CQRS架构 +- **Events** - 事件系统,实现组件间松耦合通信 +- **IoC** - 轻量级依赖注入容器 +- **Property** - 可绑定属性,支持数据绑定和响应式编程 +- **Utility** - 无状态工具类 +- **Pool** - 对象池系统,减少GC压力 +- **Extensions** - 框架扩展方法 +- **Logging** - 日志系统 +- **Environment** - 环境配置管理 -## 框架概述 +## 设计原则 -本框架是一个与平台无关的轻量级架构,它结合了多种经典设计模式: +- 与平台解耦,不依赖特定游戏引擎 +- 接口隔离,职责单一 +- 依赖倒置,面向接口编程 +- 组合优于继承 -- **MVC 架构模式** - 清晰的层次划分 -- **CQRS 模式** - 命令查询职责分离 -- **IoC/DI** - 依赖注入和控制反转 -- **事件驱动** - 松耦合的组件通信 -- **响应式编程** - 可绑定属性和数据流 -- **阶段式生命周期管理** - 精细化的架构状态控制 +## 详细文档 -**重要说明**:GFramework.Core 是与平台无关的核心模块,不包含任何 Godot 特定代码。Godot 集成功能在 GFramework.Godot 包中实现。 - -### 核心特性 - -- **清晰的分层架构** - Model、View、Controller、System、Utility 各司其职 -- **类型安全** - 基于泛型的组件获取和事件系统 -- **松耦合** - 通过事件和接口实现组件解耦 -- **易于测试** - 依赖注入和纯函数设计 -- **可扩展** - 基于接口的规则体系 -- **生命周期管理** - 自动的注册和注销机制 -- **模块化** - 支持架构模块安装 -- **平台无关** - Core 模块可以在任何 .NET 环境中使用 - -## 核心概念 - -### 五层架构 - -``` -┌─────────────────────────────────────────┐ -│ View / UI │ UI 层:用户界面 -├─────────────────────────────────────────┤ -│ Controller │ 控制层:处理用户输入 -├─────────────────────────────────────────┤ -│ System │ 逻辑层:业务逻辑 -├─────────────────────────────────────────┤ -│ Model │ 数据层:游戏状态 -├─────────────────────────────────────────┤ -│ Utility │ 工具层:无状态工具 -└─────────────────────────────────────────┘ -``` - -### 横切关注点 - -``` -Command ──┐ -Query ──┼──→ 跨层操作(修改/查询数据) -Event ──┘ -``` - -### 架构阶段 - -``` -初始化:Init → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit → BeforeSystemInit → AfterSystemInit → Ready -销毁:Destroy → Destroying → Destroyed -``` - -## 架构图 - -### 整体架构 - -``` - ┌──────────────────┐ - │ Architecture │ ← 管理所有组件 - └────────┬─────────┘ - │ - ┌────────────────────┼────────────────────┐ - │ │ │ - ┌───▼────┐ ┌───▼────┐ ┌───▼─────┐ - │ Model │ │ System │ │ Utility │ - │ 层 │ │ 层 │ │ 层 │ - └───┬────┘ └───┬────┘ └────────┘ - │ │ - │ ┌─────────────┤ - │ │ │ - ┌───▼────▼───┐ ┌───▼──────┐ - │ Controller │ │ Command/ │ - │ 层 │ │ Query │ - └─────┬──────┘ └──────────┘ - │ - ┌─────▼─────┐ - │ View │ - │ UI │ - └───────────┘ -``` - -### 数据流向 - -``` -用户输入 → Controller → Command → System → Model → Event → Controller → View 更新 - -查询流程:Controller → Query → Model → 返回数据 -``` - -## 快速开始 - -本框架采用"约定优于配置"的设计理念,只需 4 步即可搭建完整的架构。 - -### 为什么需要这个框架? - -在传统开发中,我们经常遇到这些问题: - -- 代码耦合严重:UI 直接访问游戏逻辑,逻辑直接操作 UI -- 难以维护:修改一个功能需要改动多个文件 -- 难以测试:业务逻辑和 UI 混在一起无法独立测试 -- 难以复用:代码紧密耦合,无法在其他项目中复用 - -本框架通过清晰的分层解决这些问题。 - -### 1. 定义架构(Architecture) - -**作用**:Architecture 是整个应用的"中央调度器",负责管理所有组件的生命周期。 - -```csharp -using GFramework.Core.architecture; - -public class GameArchitecture : Architecture -{ - protected override void Init() - { - // 注册 Model - 游戏数据 - RegisterModel(new PlayerModel()); - - // 注册 System - 业务逻辑 - RegisterSystem(new CombatSystem()); - - // 注册 Utility - 工具类 - RegisterUtility(new StorageUtility()); - } -} -``` - -**优势**: - -- **依赖注入**:组件通过上下文获取架构引用 -- **集中管理**:所有组件注册在一处,一目了然 -- **生命周期管理**:自动初始化和销毁 -- **平台无关**:可以在任何 .NET 环境中使用 - -### 2. 定义 Model(数据层) - -**作用**:Model 是应用的"数据库",只负责存储和管理状态。 - -```csharp -public class PlayerModel : AbstractModel -{ - // 使用 BindableProperty 实现响应式数据 - public BindableProperty Health { get; } = new(100); - public BindableProperty Gold { get; } = new(0); - - protected override void OnInit() - { - // Model 中可以监听自己的数据变化 - Health.Register(hp => - { - if (hp <= 0) this.SendEvent(new PlayerDiedEvent()); - }); - } -} - -// 也可以不使用 BindableProperty -public class PlayerModel : AbstractModel -{ - public int Health { get; private set; } - public int Gold { get; private set; } - - protected override void OnInit() - { - Health = 100; - Gold = 0; - } -} -``` - -**优势**: - -- **数据响应式**:BindableProperty 让数据变化自动通知监听者 -- **职责单一**:只存储数据,不包含复杂业务逻辑 -- **易于测试**:可以独立测试数据逻辑 - -### 3. 定义 System(业务逻辑层) - -**作用**:System 是应用的"大脑",处理所有业务逻辑。 - -```csharp -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() - { - // System 通过事件驱动,响应游戏中的各种事件 - this.RegisterEvent(OnEnemyAttack); - } - - private void OnEnemyAttack(EnemyAttackEvent e) - { - var playerModel = this.GetModel(); - - // 处理业务逻辑:计算伤害、更新数据 - playerModel.Health.Value -= e.Damage; - - // 发送事件通知其他组件 - this.SendEvent(new PlayerTookDamageEvent { Damage = e.Damage }); - } -} -``` - -**优势**: - -- **事件驱动**:通过事件解耦,不同 System 之间松耦合 -- **可组合**:多个 System 协同工作,每个专注自己的领域 -- **易于扩展**:新增功能只需添加新的 System 和事件监听 - -### 4. 定义 Controller(控制层) - -**作用**:Controller 是"桥梁",连接 UI 和业务逻辑。 - -```csharp -public class PlayerController : IController -{ - // 通过依赖注入获取架构 - private readonly IArchitecture _architecture; - - public PlayerController(IArchitecture architecture) - { - _architecture = architecture; - } - - // 监听模型变化 - public void Initialize() - { - var playerModel = _architecture.GetModel(); - - // 数据绑定:Model 数据变化自动更新 UI - playerModel.Health.RegisterWithInitValue(OnHealthChanged); - } - - private void OnHealthChanged(int hp) - { - // 更新 UI 显示 - UpdateHealthDisplay(hp); - } - - private void UpdateHealthDisplay(int hp) { /* UI 更新逻辑 */ } -} -``` - -**优势**: - -- **自动更新 UI**:通过 BindableProperty,数据变化自动反映到界面 -- **分离关注点**:UI 逻辑和业务逻辑完全分离 -- **易于测试**:可以通过依赖注入模拟架构进行测试 - -### 完成!现在你有了一个完整的架构 - -这 4 步完成后,你就拥有了: - -- **清晰的数据层**(Model) -- **独立的业务逻辑**(System) -- **灵活的控制层**(Controller) -- **统一的生命周期管理**(Architecture) - -### 下一步该做什么? - -1. **添加 Command**:封装用户操作(如购买物品、使用技能) -2. **添加 Query**:封装数据查询(如查询背包物品数量) -3. **添加更多 System**:如任务系统、背包系统、商店系统 -4. **使用 Utility**:添加工具类(如存档工具、数学工具) -5. **使用模块**:通过 IArchitectureModule 扩展架构功能 - -## 包说明 - -| 包名 | 职责 | 文档 | -|------------------|-----------------|------------------------------| -| **architecture** | 架构核心,管理所有组件生命周期 | [查看](architecture/README.md) | -| **constants** | 框架常量定义 | 本文档 | -| **model** | 数据模型层,存储状态 | [查看](model/README.md) | -| **system** | 业务逻辑层,处理业务规则 | [查看](system/README.md) | -| **controller** | 控制器层,连接视图和逻辑 | (在 Abstractions 中) | -| **utility** | 工具类层,提供无状态工具 | [查看](utility/README.md) | -| **command** | 命令模式,封装写操作 | [查看](command/README.md) | -| **query** | 查询模式,封装读操作 | [查看](query/README.md) | -| **events** | 事件系统,组件间通信 | [查看](events/README.md) | -| **property** | 可绑定属性,响应式编程 | [查看](property/README.md) | -| **ioc** | IoC 容器,依赖注入 | [查看](ioc/README.md) | -| **rule** | 规则接口,定义组件约束 | [查看](rule/README.md) | -| **extensions** | 扩展方法,简化 API 调用 | [查看](extensions/README.md) | -| **logging** | 日志系统,记录运行日志 | [查看](logging/README.md) | -| **environment** | 环境接口,提供运行环境信息 | [查看](environment/README.md) | - -## 组件联动 - -### 1. 初始化流程 - -``` -创建 Architecture 实例 - └─> Init() - ├─> RegisterModel → Model.SetContext() → Model.Init() - ├─> RegisterSystem → System.SetContext() → System.Init() - └─> RegisterUtility → Utility 注册到容器 -``` - -### 2. Command 执行流程 - -``` -Controller.SendCommand(command) - └─> command.Execute() - └─> command.OnDo() // 子类实现 - ├─> GetModel() // 获取数据 - ├─> 修改 Model 数据 - └─> SendEvent() // 发送事件 -``` - -### 3. Event 传播流程 - -``` -组件.SendEvent(event) - └─> TypeEventSystem.Send(event) - └─> 通知所有订阅者 - ├─> Controller 响应 → 更新 UI - ├─> System 响应 → 执行逻辑 - └─> Model 响应 → 更新状态 -``` - -### 4. BindableProperty 数据绑定 - -``` -Model: BindableProperty Health = new(100); -Controller: Health.RegisterWithInitValue(hp => UpdateUI(hp)) -修改值: Health.Value = 50 → 触发所有回调 → 更新 UI -``` - -## 最佳实践 - -### 1. 分层职责原则 - -每一层都有明确的职责边界,遵循这些原则能让代码更清晰、更易维护。 - -**Model 层**: - -```csharp -// 好:只存储数据 -public class PlayerModel : AbstractModel -{ - public BindableProperty Health { get; } = new(100); - protected override void OnInit() { } -} - -// 坏:包含业务逻辑 -public class PlayerModel : AbstractModel -{ - public void TakeDamage(int damage) // 业务逻辑应在 System - { - Health.Value -= damage; - if (Health.Value <= 0) Die(); - } -} -``` - -**System 层**: - -```csharp -// 好:处理业务逻辑 -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() - { - this.RegisterEvent(OnAttack); - } - - private void OnAttack(AttackEvent e) - { - var target = this.GetModel(); - int finalDamage = CalculateDamage(e.BaseDamage, target); - target.Health.Value -= finalDamage; - } -} -``` - -### 2. 通信方式选择指南 - -| 通信方式 | 使用场景 | 优势 | -|----------------------|-----------|----------| -| **Command** | 用户操作、修改状态 | 可撤销、可记录 | -| **Query** | 查询数据、检查条件 | 明确只读意图 | -| **Event** | 通知其他组件 | 松耦合、可扩展 | -| **BindableProperty** | 数据变化通知 | 自动化、不会遗漏 | - -### 3. 生命周期管理 - -**为什么需要注销?** - -忘记注销监听器会导致: - -- **内存泄漏**:对象无法被 GC 回收 -- **逻辑错误**:已销毁的对象仍在响应事件 - -```csharp -// 使用 UnRegisterList 统一管理 -private IUnRegisterList _unregisterList = new UnRegisterList(); - -public void Initialize() -{ - this.RegisterEvent(OnEvent1) - .AddToUnregisterList(_unregisterList); - - model.Property.Register(OnPropertyChanged) - .AddToUnregisterList(_unregisterList); -} - -public void Cleanup() -{ - _unregisterList.UnRegisterAll(); -} -``` - -### 4. 性能优化技巧 - -```csharp -// 低效:每帧都查询 -var model = _architecture.GetModel(); // 频繁调用 - -// 高效:缓存引用 -private PlayerModel _playerModel; - -public void Initialize() -{ - _playerModel = _architecture.GetModel(); // 只查询一次 -} -``` - -## 设计理念 - -框架的设计遵循 SOLID 原则和经典设计模式。 - -### 1. 单一职责原则(SRP) - -- **Model**:只负责存储数据 -- **System**:只负责处理业务逻辑 -- **Controller**:只负责协调和输入处理 -- **Utility**:只负责提供工具方法 - -### 2. 开闭原则(OCP) - -- 通过**事件系统**添加新功能,无需修改现有代码 -- 新的 System 可以监听现有事件,插入自己的逻辑 - -### 3. 依赖倒置原则(DIP) - -- 所有组件通过接口交互 -- 通过 IoC 容器注入依赖 -- 易于替换实现和编写测试 - -### 4. 接口隔离原则(ISP) - -```csharp -// 小而专注的接口 -public interface ICanGetModel : IBelongToArchitecture { } -public interface ICanSendCommand : IBelongToArchitecture { } -public interface ICanRegisterEvent : IBelongToArchitecture { } - -// 组合需要的能力 -public interface IController : - ICanGetModel, - ICanSendCommand, - ICanRegisterEvent { } -``` - -### 5. 组合优于继承 - -通过接口组合获得能力,而不是通过继承。 - -### 框架核心设计模式 - -| 设计模式 | 应用位置 | 解决的问题 | 带来的好处 | -|-----------|------------|----------|--------| -| **工厂模式** | IoC 容器 | 组件的创建和管理 | 解耦创建逻辑 | -| **观察者模式** | Event 系统 | 组件间的通信 | 松耦合通信 | -| **命令模式** | Command | 封装操作请求 | 支持撤销重做 | -| **策略模式** | System | 不同的业务逻辑 | 易于切换策略 | -| **依赖注入** | 整体架构 | 组件间的依赖 | 自动管理依赖 | -| **模板方法** | Abstract 类 | 定义算法骨架 | 统一流程规范 | - -### 平台无关性 - -- **GFramework.Core**:纯 .NET 库,无任何平台特定代码 -- **GFramework.Godot**:Godot 特定实现,包含 Node 扩展、GodotLogger 等 -- 可以轻松将 Core 框架移植到其他平台(Unity、.NET MAUI 等) - ---- - -**版本**: 1.0.0 -**许可证**: Apache 2.0 +参见 [docs/core/](../docs/core/) 目录下的详细文档。 diff --git a/GFramework.Game.Abstractions/README.md b/GFramework.Game.Abstractions/README.md index 9ac048d8..fe14fe80 100644 --- a/GFramework.Game.Abstractions/README.md +++ b/GFramework.Game.Abstractions/README.md @@ -1,14 +1,13 @@ # GFramework.Game.Abstractions -`GFramework.Game.Abstractions` 提供 `GFramework.Game` 的抽象层接口定义,用于解耦业务逻辑与具体实现。 +GFramework.Game 模块的抽象层,提供游戏业务相关的接口定义。 ## 主要内容 -- 游戏业务常用抽象(数据、场景、设置、存储、UI 等) -- 与 `GFramework.Core.Abstractions` 配合使用的接口契约 -- 供上层应用或扩展模块进行面向接口编程 +- 游戏业务常用抽象接口 +- 与 GFramework.Core.Abstractions 配合使用的契约 ## 使用建议 -- 若你需要直接使用完整游戏扩展能力,优先安装 `GeWuYou.GFramework.Game`。 -- 若你在做模块拆分、测试替身(Mock)或跨实现解耦,可单独依赖本包。 +- 若需直接使用完整游戏扩展能力,优先使用 GFramework.Game +- 若在做模块拆分或需要解耦,可单独依赖本包 diff --git a/GFramework.Game/README.md b/GFramework.Game/README.md index 218c2d67..e3e255ee 100644 --- a/GFramework.Game/README.md +++ b/GFramework.Game/README.md @@ -1,1406 +1,16 @@ # GFramework.Game -> 游戏特定功能抽象 - 为游戏开发提供专门的工具和系统 +GFramework 框架的游戏通用模块,提供游戏开发常用的功能。 -GFramework.Game 是 GFramework 框架的游戏特定功能模块,提供了游戏开发中常用的抽象和工具,包括资产管理、存储系统、序列化等核心功能。 +## 主要功能 -## 📋 目录 - -- [概述](#概述) -- [核心特性](#核心特性) -- [架构模块系统](#架构模块系统) -- [资产管理](#资产管理) -- [存储系统](#存储系统) -- [序列化系统](#序列化系统) -- [使用示例](#使用示例) -- [最佳实践](#最佳实践) -- [性能特性](#性能特性) - -## 概述 - -GFramework.Game 为游戏开发提供了专门的功能模块,与 GFramework.Core 的平台无关特性完美结合,为游戏项目提供了一整套完整的解决方案。 - -### 核心设计理念 - -- **游戏导向**:专门针对游戏开发场景设计 -- **模块化架构**:可插拔的模块系统,按需组合 -- **数据持久化**:完善的存档和数据管理方案 -- **资源管理**:高效的资源加载和管理机制 - -## 核心特性 - -### 🏗️ 模块化架构 - -- **AbstractModule**:可重用的架构模块基类 -- **生命周期管理**:与框架生命周期深度集成 -- **依赖注入**:模块间的依赖自动管理 -- **配置驱动**:灵活的模块配置系统 - -### 📦 资产管理 - -- **统一资源目录**:集中化的资源注册和查询 -- **类型安全**:编译时类型检查和泛型支持 -- **重复检测**:自动检测资源重复注册 -- **映射支持**:灵活的资源映射和别名系统 - -### 💾 存储系统 - -- **分层存储**:命名空间支持的存储隔离 -- **多格式支持**:JSON、二进制等多种存储格式 -- **异步操作**:完整的异步存储 API -- **版本兼容**:存档版本管理和迁移支持 - -### 🔄 序列化系统 - -- **JSON 集成**:基于 Newtonsoft.Json 的序列化 -- **自定义序列化**:支持自定义序列化逻辑 -- **性能优化**:序列化缓存和优化策略 -- **类型安全**:强类型的序列化和反序列化 - -## 架构模块系统 - -### AbstractModule 基础使用 - -```csharp -using GFramework.Game.architecture; - -public class AudioModule : AbstractModule -{ - public override void Install(IArchitecture architecture) - { - // 注册音频相关系统 - architecture.RegisterSystem(new AudioSystem()); - architecture.RegisterSystem(new MusicSystem()); - - // 注册音频工具 - architecture.RegisterUtility(new AudioUtility()); - architecture.RegisterUtility(new MusicUtility()); - } - - public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - switch (phase) - { - case ArchitecturePhase.BeforeModelInit: - // 在模型初始化前准备音频资源 - PreloadAudioResources(); - break; - - case ArchitecturePhase.Ready: - // 架构准备就绪,开始播放背景音乐 - StartBackgroundMusic(); - break; - - case ArchitecturePhase.Destroying: - // 清理音频资源 - CleanupAudioResources(); - break; - } - } - - private void PreloadAudioResources() - { - // 预加载音频资源 - var audioUtility = architecture.GetUtility(); - audioUtility.PreloadAudio("background_music"); - audioUtility.PreloadAudio("shoot_sound"); - audioUtility.PreloadAudio("explosion_sound"); - } - - private void StartBackgroundMusic() - { - var musicSystem = architecture.GetSystem(); - musicSystem.PlayBackgroundMusic("background_music"); - } - - private void CleanupAudioResources() - { - var audioUtility = architecture.GetUtility(); - audioUtility.UnloadAllAudio(); - } -} -``` - -### 复杂模块示例 - -```csharp -public class SaveModule : AbstractModule -{ - private ISaveSystem _saveSystem; - private IDataMigrationManager _migrationManager; - - public override void Install(IArchitecture architecture) - { - // 注册存储相关组件 - _saveSystem = new SaveSystem(); - architecture.RegisterUtility(_saveSystem); - - _migrationManager = new DataMigrationManager(); - architecture.RegisterUtility(_migrationManager); - - // 注册数据版本管理 - architecture.RegisterSystem(new SaveDataVersionSystem()); - } - - public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - switch (phase) - { - case ArchitecturePhase.AfterModelInit: - // 在模型初始化后设置数据迁移 - SetupDataMigrations(); - break; - - case ArchitecturePhase.Ready: - // 尝试自动加载存档 - TryAutoLoadSave(); - break; - } - } - - private void SetupDataMigrations() - { - // 设置数据版本迁移 - _migrationManager.RegisterMigration(1, 2, MigratePlayerDataV1ToV2); - _migrationManager.RegisterMigration(2, 3, MigratePlayerDataV2ToV3); - } - - private void TryAutoLoadSave() - { - if (_saveSystem.HasAutoSave()) - { - _saveSystem.LoadAutoSave(); - } - } - - private PlayerData MigratePlayerDataV1ToV2(PlayerData v1Data) - { - return new PlayerData - { - // 迁移逻辑 - Name = v1Data.Name, - Health = v1Data.Health, - // 新增字段 - MaxHealth = 100, - Level = 1 - }; - } - - private PlayerData MigratePlayerDataV2ToV3(PlayerData v2Data) - { - return new PlayerData - { - // 迁移逻辑 - Name = v2Data.Name, - Health = v2Data.Health, - MaxHealth = v2Data.MaxHealth, - Level = v2Data.Level, - // 新增字段 - Experience = 0, - Skills = new List() - }; - } -} -``` - -### 模块配置 - -```csharp -public class ModuleConfig -{ - public string SaveDirectory { get; set; } = "saves"; - public int AutoSaveInterval { get; set; } = 300; // 5分钟 - public bool EnableDataCompression { get; set; } = true; - public int MaxSaveSlots { get; set; } = 10; -} - -public class ConfigurableSaveModule : AbstractModule -{ - private ModuleConfig _config; - - public ConfigurableSaveModule(ModuleConfig config) - { - _config = config; - } - - public override void Install(IArchitecture architecture) - { - var saveSystem = new SaveSystem(_config); - architecture.RegisterUtility(saveSystem); - - // 配置自动保存 - if (_config.AutoSaveInterval > 0) - { - architecture.RegisterSystem(new AutoSaveSystem(_config.AutoSaveInterval)); - } - } -} -``` - -## 资产管理 - -### AbstractAssetCatalogUtility 基础使用 - -```csharp -using GFramework.Game.assets; - -public class GameAssetCatalog : AbstractAssetCatalogUtility -{ - public override void Initialize() - { - base.Initialize(); - - // 注册场景资产 - RegisterSceneUnit("Player", "res://scenes/Player.tscn"); - RegisterSceneUnit("Enemy", "res://scenes/Enemy.tscn"); - RegisterSceneUnit("Bullet", "res://scenes/Bullet.tscn"); - - // 注册场景页面 - RegisterScenePage("MainMenu", "res://ui/MainMenu.tscn"); - RegisterScenePage("GameUI", "res://ui/GameUI.tscn"); - RegisterScenePage("PauseMenu", "res://ui/PauseMenu.tscn"); - - // 注册通用资产 - RegisterAsset("PlayerTexture", "res://textures/player.png"); - RegisterAsset("EnemyTexture", "res://textures/enemy.png"); - RegisterAsset("ShootSound", "res://audio/shoot.wav"); - RegisterAsset("ExplosionSound", "res://audio/explosion.wav"); - } - - // 自定义资产验证 - protected override bool ValidateAsset(string key, string path) - { - if (!FileAccess.FileExists(path)) - { - GD.PrintErr($"Asset file not found: {path}"); - return false; - } - - return true; - } - - // 资产加载完成回调 - protected override void OnAssetLoaded(string key, object asset) - { - GD.Print($"Asset loaded: {key}"); - - // 对特定资产进行额外处理 - if (key == "PlayerTexture") - { - var texture = (Texture2D)asset; - // 预处理纹理... - } - } -} -``` - -### 资产映射系统 - -```csharp -public class AssetMapping -{ - public string Key { get; set; } - public string Path { get; set; } - public Type Type { get; set; } - public Dictionary Metadata { get; set; } = new(); -} - -public class AdvancedAssetCatalog : AbstractAssetCatalogUtility -{ - public override void Initialize() - { - base.Initialize(); - - // 使用映射对象注册资产 - RegisterAsset(new AssetMapping - { - Key = "Player", - Path = "res://scenes/Player.tscn", - Type = typeof(PackedScene), - Metadata = new Dictionary - { - ["category"] = "character", - ["tags"] = new[] { "player", "hero", "controlled" }, - ["health"] = 100, - ["speed"] = 5.0f - } - }); - - // 批量注册 - RegisterAssetsFromDirectory("res://textures/", "*.png", "texture"); - RegisterAssetsFromDirectory("res://audio/", "*.wav", "sound"); - } - - private void RegisterAssetsFromDirectory(string directory, string pattern, string prefix) - { - var dir = DirAccess.Open(directory); - if (dir == null) return; - - dir.ListDirBegin(); - var fileName = dir.GetNext(); - - while (!string.IsNullOrEmpty(fileName)) - { - if (fileName.EndsWith(pattern.Substring(1))) - { - var key = $"{prefix}{Path.GetFileNameWithoutExtension(fileName)}"; - var path = Path.Combine(directory, fileName); - - RegisterAsset(key, path); - } - - fileName = dir.GetNext(); - } - - dir.ListDirEnd(); - } -} -``` - -### 资产工厂模式 - -```csharp -public interface IAssetFactory -{ - T Create(string key); - bool CanCreate(string key); -} - -public class PlayerFactory : IAssetFactory -{ - private readonly AbstractAssetCatalogUtility _catalog; - - public PlayerFactory(AbstractAssetCatalogUtility catalog) - { - _catalog = catalog; - } - - public Player Create(string key) - { - var scene = _catalog.GetScene(key); - var player = scene.Instantiate(); - - // 配置玩家 - player.Health = GetPlayerHealth(key); - player.Speed = GetPlayerSpeed(key); - - return player; - } - - public bool CanCreate(string key) - { - return _catalog.HasScene(key) && key.StartsWith("Player"); - } - - private int GetPlayerHealth(string key) - { - var metadata = _catalog.GetAssetMetadata(key); - return metadata?.GetValueOrDefault("health", 100) ?? 100; - } - - private float GetPlayerSpeed(string key) - { - var metadata = _catalog.GetAssetMetadata(key); - return metadata?.GetValueOrDefault("speed", 5.0f) ?? 5.0f; - } -} -``` - -## 存储系统 - -### ScopedStorage 分层存储 - -```csharp -using GFramework.Game.storage; - -public class GameDataManager -{ - private readonly IStorage _rootStorage; - private readonly IStorage _playerStorage; - private readonly IStorage _saveStorage; - - public GameDataManager(IStorage rootStorage) - { - _rootStorage = rootStorage; - - // 创建分层存储 - _playerStorage = new ScopedStorage(rootStorage, "player"); - _saveStorage = new ScopedStorage(rootStorage, "saves"); - } - - public void SavePlayerData(string playerId, PlayerData data) - { - _playerStorage.Write($"{playerId}/profile", data); - _playerStorage.Write($"{playerId}/inventory", data.Inventory); - _playerStorage.Write($"{playerId}/stats", data.Stats); - } - - public PlayerData LoadPlayerData(string playerId) - { - var profile = _playerStorage.Read($"{playerId}/profile"); - var inventory = _playerStorage.Read($"{playerId}/inventory", new Inventory()); - var stats = _playerStorage.Read($"{playerId}/stats", new PlayerStats()); - - return new PlayerData - { - Profile = profile, - Inventory = inventory, - Stats = stats - }; - } - - public void SaveGame(int slotId, SaveData data) - { - _saveStorage.Write($"slot_{slotId}", data); - _saveStorage.Write($"slot_{slotId}/timestamp", DateTime.Now); - _saveStorage.Write($"slot_{slotId}/version", data.Version); - } - - public SaveData LoadGame(int slotId) - { - return _saveStorage.Read($"slot_{slotId}"); - } - - public List GetSaveSlotInfos() - { - var infos = new List(); - - for (int i = 1; i <= 10; i++) - { - var timestamp = _saveStorage.Read($"slot_{i}/timestamp"); - var version = _saveStorage.Read($"slot_{i}/version"); - - if (timestamp != default) - { - infos.Add(new SaveSlotInfo - { - SlotId = i, - Timestamp = timestamp, - Version = version - }); - } - } - - return infos; - } -} -``` - -### 自定义存储实现 - -```csharp -public class EncryptedStorage : IStorage -{ - private readonly IStorage _innerStorage; - private readonly IEncryptor _encryptor; - - public EncryptedStorage(IStorage innerStorage, IEncryptor encryptor) - { - _innerStorage = innerStorage; - _encryptor = encryptor; - } - - public void Write(string key, T data) - { - var json = JsonConvert.SerializeObject(data); - var encrypted = _encryptor.Encrypt(json); - _innerStorage.Write(key, encrypted); - } - - public T Read(string key, T defaultValue = default) - { - var encrypted = _innerStorage.Read(key); - if (string.IsNullOrEmpty(encrypted)) - return defaultValue; - - var json = _encryptor.Decrypt(encrypted); - return JsonConvert.DeserializeObject(json); - } - - public async Task WriteAsync(string key, T data) - { - var json = JsonConvert.SerializeObject(data); - var encrypted = _encryptor.Encrypt(json); - await _innerStorage.WriteAsync(key, encrypted); - } - - public async Task ReadAsync(string key, T defaultValue = default) - { - var encrypted = await _innerStorage.ReadAsync(key); - if (string.IsNullOrEmpty(encrypted)) - return defaultValue; - - var json = _encryptor.Decrypt(encrypted); - return JsonConvert.DeserializeObject(json); - } - - public bool Has(string key) - { - return _innerStorage.Has(key); - } - - public void Delete(string key) - { - _innerStorage.Delete(key); - } - - public void Clear() - { - _innerStorage.Clear(); - } -} -``` - -### 存储缓存层 - -```csharp -public class CachedStorage : IStorage -{ - private readonly IStorage _innerStorage; - private readonly Dictionary _cache; - private readonly Dictionary _cacheTimestamps; - private readonly TimeSpan _cacheExpiry; - - public CachedStorage(IStorage innerStorage, TimeSpan cacheExpiry = default) - { - _innerStorage = innerStorage; - _cacheExpiry = cacheExpiry == default ? TimeSpan.FromMinutes(5) : cacheExpiry; - _cache = new Dictionary(); - _cacheTimestamps = new Dictionary(); - } - - public T Read(string key, T defaultValue = default) - { - if (_cache.TryGetValue(key, out var cachedValue) && - !IsCacheExpired(key)) - { - return (T)cachedValue; - } - - var value = _innerStorage.Read(key, defaultValue); - UpdateCache(key, value); - - return value; - } - - public void Write(string key, T data) - { - _innerStorage.Write(key, data); - UpdateCache(key, data); - } - - public async Task ReadAsync(string key, T defaultValue = default) - { - if (_cache.TryGetValue(key, out var cachedValue) && - !IsCacheExpired(key)) - { - return (T)cachedValue; - } - - var value = await _innerStorage.ReadAsync(key, defaultValue); - UpdateCache(key, value); - - return value; - } - - public async Task WriteAsync(string key, T data) - { - await _innerStorage.WriteAsync(key, data); - UpdateCache(key, data); - } - - public bool Has(string key) - { - return _cache.ContainsKey(key) || _innerStorage.Has(key); - } - - public void Delete(string key) - { - _cache.Remove(key); - _cacheTimestamps.Remove(key); - _innerStorage.Delete(key); - } - - public void Clear() - { - _cache.Clear(); - _cacheTimestamps.Clear(); - _innerStorage.Clear(); - } - - public void ClearCache() - { - _cache.Clear(); - _cacheTimestamps.Clear(); - } - - private bool IsCacheExpired(string key) - { - if (!_cacheTimestamps.TryGetValue(key, out var timestamp)) - return true; - - return DateTime.Now - timestamp > _cacheExpiry; - } - - private void UpdateCache(string key, T value) - { - _cache[key] = value; - _cacheTimestamps[key] = DateTime.Now; - } -} -``` - -## 序列化系统 - -### JsonSerializer 使用 - -```csharp -using GFramework.Game.serializer; - -public class GameDataSerializer -{ - private readonly JsonSerializer _serializer; - - public GameDataSerializer() - { - _serializer = new JsonSerializer(new JsonSerializerSettings - { - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Populate, - TypeNameHandling = TypeNameHandling.Auto - }); - - // 自定义转换器 - _serializer.Converters.Add(new Vector2JsonConverter()); - _serializer.Converters.Add(new ColorJsonConverter()); - _serializer.Converters.Add(new GodotResourceJsonConverter()); - } - - public string Serialize(T data) - { - return _serializer.Serialize(data); - } - - public T Deserialize(string json) - { - return _serializer.Deserialize(json); - } - - public void SerializeToFile(string path, T data) - { - var json = Serialize(data); - FileAccess.Open(path, FileAccess.ModeFlags.Write).StoreString(json); - } - - public T DeserializeFromFile(string path) - { - if (!FileAccess.FileExists(path)) - return default(T); - - var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); - var json = file.GetAsText(); - file.Close(); - - return Deserialize(json); - } -} -``` - -### 自定义 JSON 转换器 - -```csharp -public class Vector2JsonConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer) - { - writer.WriteStartObject(); - writer.WritePropertyName("x"); - writer.WriteValue(value.X); - writer.WritePropertyName("y"); - writer.WriteValue(value.Y); - writer.WriteEndObject(); - } - - public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return Vector2.Zero; - - var obj = serializer.Deserialize>(reader); - return new Vector2(obj["x"], obj["y"]); - } -} - -public class ColorJsonConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer) - { - writer.WriteValue(value.ToHtml()); - } - - public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return Colors.White; - - var html = reader.Value.ToString(); - return Color.FromHtml(html); - } -} - -public class GodotResourceJsonConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, Resource value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - writer.WriteStartObject(); - writer.WritePropertyName("$type"); - writer.WriteValue(value.GetType().Name); - writer.WritePropertyName("path"); - writer.WriteValue(value.ResourcePath); - writer.WriteEndObject(); - } - - public override Resource ReadJson(JsonReader reader, Type objectType, Resource existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return null; - - var obj = serializer.Deserialize>(reader); - var path = obj["path"]; - - return GD.Load(path); - } -} -``` - -### 版本化序列化 - -```csharp -public class VersionedData -{ - public int Version { get; set; } - public Dictionary Data { get; set; } = new(); -} - -public class VersionedSerializer -{ - private readonly Dictionary _typeVersions = new(); - private readonly Dictionary _migrations = new(); - - public void RegisterVersion(int version) - { - _typeVersions[typeof(T)] = version; - } - - public void RegisterMigration(int fromVersion, int toVersion, IDataMigration migration) - { - var key = GetMigrationKey(typeof(T), fromVersion, toVersion); - _migrations[key] = migration; - } - - public string Serialize(T data) - { - var versioned = new VersionedData - { - Version = _typeVersions.GetValueOrDefault(typeof(T), 1), - Data = new Dictionary - { - ["type"] = typeof(T).Name, - ["data"] = JsonConvert.SerializeObject(data) - } - }; - - return JsonConvert.SerializeObject(versioned); - } - - public T Deserialize(string json) - { - var versioned = JsonConvert.DeserializeObject(json); - var currentVersion = _typeVersions.GetValueOrDefault(typeof(T), 1); - - // 迁移数据到当前版本 - var dataJson = MigrateData(versioned.Data["data"] as string, versioned.Version, currentVersion); - - return JsonConvert.DeserializeObject(dataJson); - } - - private string MigrateData(string dataJson, int fromVersion, int toVersion) - { - var currentData = dataJson; - var currentVersion = fromVersion; - - while (currentVersion < toVersion) - { - var migrationKey = GetMigrationKey(typeof(T), currentVersion, currentVersion + 1); - - if (_migrations.TryGetValue(migrationKey, out var migration)) - { - currentData = migration.Migrate(currentData); - currentVersion++; - } - else - { - throw new InvalidOperationException($"No migration found from version {currentVersion} to {currentVersion + 1}"); - } - } - - return currentData; - } - - private int GetMigrationKey(Type type, int fromVersion, int toVersion) - { - return HashCode.Combine(type.Name, fromVersion, toVersion); - } -} - -public interface IDataMigration -{ - string Migrate(string data); -} - -public interface IDataMigration : IDataMigration -{ - T Migrate(T data); -} -``` - -## 使用示例 - -### 完整的游戏数据管理系统 - -```csharp -// 1. 定义数据模型 -public class GameProfile -{ - public string PlayerName { get; set; } - public DateTime LastPlayed { get; set; } - public int TotalPlayTime { get; set; } - public List UnlockedAchievements { get; set; } = new(); -} - -public class GameSettings -{ - public float MasterVolume { get; set; } = 1.0f; - public float MusicVolume { get; set; } = 0.8f; - public float SFXVolume { get; set; } = 0.9f; - public GraphicsSettings Graphics { get; set; } = new(); - public InputSettings Input { get; set; } = new(); -} - -// 2. 创建游戏管理器 -[ContextAware] -[Log] -public partial class GameManager : Node, IController -{ - private GameAssetCatalog _assetCatalog; - private GameDataManager _dataManager; - private GameDataSerializer _serializer; - - protected override void OnInit() - { - // 初始化资产目录 - _assetCatalog = new GameAssetCatalog(); - _assetCatalog.Initialize(); - - // 初始化存储系统 - var rootStorage = new FileStorage("user://data/"); - var cachedStorage = new CachedStorage(rootStorage); - _dataManager = new GameDataManager(cachedStorage); - - // 初始化序列化器 - _serializer = new GameDataSerializer(); - - Logger.Info("Game manager initialized"); - } - - public void StartNewGame(string playerName) - { - Logger.Info($"Starting new game for player: {playerName}"); - - // 创建新的游戏档案 - var profile = new GameProfile - { - PlayerName = playerName, - LastPlayed = DateTime.Now, - TotalPlayTime = 0 - }; - - _dataManager.SavePlayerData("current", profile); - - // 加载初始场景 - LoadInitialScene(); - - Context.SendEvent(new NewGameStartedEvent { PlayerName = playerName }); - } - - public void LoadGame(int slotId) - { - Logger.Info($"Loading game from slot {slotId}"); - - try - { - var saveData = _dataManager.LoadGame(slotId); - if (saveData != null) - { - // 恢复游戏状态 - RestoreGameState(saveData); - - Context.SendEvent(new GameLoadedEvent { SlotId = slotId }); - Logger.Info("Game loaded successfully"); - } - else - { - Logger.Warning($"No save data found in slot {slotId}"); - Context.SendEvent(new GameLoadFailedEvent { SlotId = slotId }); - } - } - catch (Exception ex) - { - Logger.Error($"Failed to load game: {ex.Message}"); - Context.SendEvent(new GameLoadFailedEvent { SlotId = slotId, Error = ex.Message }); - } - } - - public void SaveGame(int slotId) - { - Logger.Info($"Saving game to slot {slotId}"); - - try - { - var saveData = CreateSaveData(); - _dataManager.SaveGame(slotId, saveData); - - Context.SendEvent(new GameSavedEvent { SlotId = slotId }); - Logger.Info("Game saved successfully"); - } - catch (Exception ex) - { - Logger.Error($"Failed to save game: {ex.Message}"); - Context.SendEvent(new GameSaveFailedEvent { SlotId = slotId, Error = ex.Message }); - } - } - - private void LoadInitialScene() - { - var playerScene = _assetCatalog.GetScene("Player"); - var player = playerScene.Instantiate(); - - var gameWorldScene = _assetCatalog.GetScene("GameWorld"); - var gameWorld = gameWorldScene.Instantiate(); - - AddChild(gameWorld); - gameWorld.AddChild(player); - } - - private void RestoreGameState(SaveData saveData) - { - // 恢复玩家位置 - var playerScene = _assetCatalog.GetScene("Player"); - var player = playerScene.Instantiate(); - player.Position = saveData.PlayerPosition; - - // 恢复游戏世界 - var gameWorldScene = _assetCatalog.GetScene("GameWorld"); - var gameWorld = gameWorldScene.Instantiate(); - - AddChild(gameWorld); - gameWorld.AddChild(player); - - // 恢复其他游戏状态 - Context.GetModel().Health.Value = saveData.PlayerHealth; - Context.GetModel().CurrentLevel.Value = saveData.CurrentLevel; - Context.GetModel().LoadFromData(saveData.Inventory); - } - - private SaveData CreateSaveData() - { - var player = GetTree().CurrentScene.FindChild("Player"); - - return new SaveData - { - PlayerPosition = player?.Position ?? Vector2.Zero, - PlayerHealth = Context.GetModel().Health.Value, - CurrentLevel = Context.GetModel().CurrentLevel.Value, - Inventory = Context.GetModel().GetData(), - Timestamp = DateTime.Now, - Version = 1 - }; - } -} -``` - -### 自动保存系统 - -```csharp -public class AutoSaveSystem : AbstractSystem -{ - private Timer _autoSaveTimer; - private int _currentSaveSlot; - private float _autoSaveInterval; - - public AutoSaveSystem(float intervalMinutes = 5.0f) - { - _autoSaveInterval = intervalMinutes * 60.0f; - } - - protected override void OnInit() - { - _autoSaveTimer = new Timer(); - _autoSaveTimer.WaitTime = _autoSaveInterval; - _autoSaveTimer.Timeout += OnAutoSave; - _autoSaveTimer.Autostart = true; - - AddChild(_autoSaveTimer); - - // 监听重要事件,立即自动保存 - this.RegisterEvent(OnImportantEvent); - this.RegisterEvent(OnImportantEvent); - this.RegisterEvent(OnImportantEvent); - - Logger.Info("Auto-save system initialized"); - } - - protected override void OnDestroy() - { - _autoSaveTimer?.Stop(); - - // 最后一次自动保存 - PerformAutoSave(); - } - - private void OnAutoSave() - { - PerformAutoSave(); - Logger.Debug("Periodic auto-save completed"); - } - - private void OnImportantEvent(IEvent e) - { - Logger.Info($"Important event {e.GetType().Name}, triggering auto-save"); - PerformAutoSave(); - } - - private void PerformAutoSave() - { - try - { - var saveData = CreateAutoSaveData(); - - // 保存到自动存档槽 - var storage = Context.GetUtility(); - storage.Write("autosave", saveData); - storage.Write("autosave/timestamp", DateTime.Now); - - Logger.Debug("Auto-save completed successfully"); - } - catch (Exception ex) - { - Logger.Error($"Auto-save failed: {ex.Message}"); - } - } - - private SaveData CreateAutoSaveData() - { - return new SaveData - { - // ... 创建保存数据 - }; - } -} -``` - -## 最佳实践 - -### 🏗️ 数据模型设计 - -#### 1. 版本化数据结构 - -```csharp -// 好的做法:版本化数据模型 -[Serializable] -public class PlayerDataV2 -{ - public int Version { get; set; } = 2; - public string Name { get; set; } - public int Health { get; set; } - public int MaxHealth { get; set; } = 100; // V2 新增 - public List Skills { get; set; } = new(); // V2 新增 -} - -// 避免:没有版本控制 -[Serializable] -public class PlayerData -{ - public string Name { get; set; } - public int Health { get; set; } - // 未来新增字段会导致兼容性问题 -} -``` - -#### 2. 数据验证 - -```csharp -public class PlayerData -{ - private string _name; - - public string Name - { - get => _name; - set - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("Player name cannot be empty"); - _name = value; - } - } - - public int Health - { - get => _health; - set => _health = Math.Max(0, value); // 确保不为负数 - } - - public void Validate() - { - if (string.IsNullOrWhiteSpace(Name)) - throw new ValidationException("Player name is required"); - - if (Health < 0) - throw new ValidationException("Health cannot be negative"); - } -} -``` - -### 💾 存储策略 - -#### 1. 分层存储命名 - -```csharp -// 好的做法:有意义的分层结构 -var playerStorage = new ScopedStorage(rootStorage, "players"); -var saveStorage = new ScopedStorage(rootStorage, "saves"); -var settingsStorage = new ScopedStorage(rootStorage, "settings"); -var tempStorage = new ScopedStorage(rootStorage, "temp"); - -// 避免的混乱命名 -var storage1 = new ScopedStorage(rootStorage, "data1"); -var storage2 = new ScopedStorage(rootStorage, "data2"); -``` - -#### 2. 存储性能优化 - -```csharp -// 好的做法:批量操作和缓存 -public class OptimizedDataManager -{ - private readonly IStorage _storage; - private readonly Dictionary _writeBuffer = new(); - - public void QueueWrite(string key, T data) - { - _writeBuffer[key] = data; - } - - public async Task FlushWritesAsync() - { - var tasks = _writeBuffer.Select(kvp => _storage.WriteAsync(kvp.Key, kvp.Value)); - await Task.WhenAll(tasks); - _writeBuffer.Clear(); - } -} - -// 避免:频繁的小写入 -public class InefficientDataManager -{ - public void UpdatePlayerStat(string stat, int value) - { - _storage.Write($"player/stats/{stat}", value); // 每次都写入磁盘 - } -} -``` - -### 🔄 序列化优化 - -#### 1. 选择合适的序列化格式 - -```csharp -// 好的做法:根据需求选择格式 -public class GameSerializer -{ - // JSON:可读性好,调试方便 - public string SerializeForDebug(object data) => JsonConvert.SerializeObject(data, Formatting.Indented); - - // 二进制:体积小,性能好 - public byte[] SerializeForStorage(object data) => MessagePackSerializer.Serialize(data); - - // 压缩:减少存储空间 - public byte[] SerializeWithCompression(object data) - { - var json = JsonConvert.SerializeObject(data); - return Compress(Encoding.UTF8.GetBytes(json)); - } -} -``` - -#### 2. 自定义序列化逻辑 - -```csharp -public class PlayerInventory -{ - public Dictionary Items { get; set; } = new(); - - [JsonIgnore] // 排除不需要序列化的属性 - public int TotalWeight => Items.Sum(kvp => GetItemWeight(kvp.Key) * kvp.Value); - - [JsonProperty("total_items")] // 自定义序列化名称 - public int TotalItems => Items.Values.Sum(); - - public bool ShouldSerializeItems() // 条件序列化 - { - return Items.Count > 0; - } - - [OnDeserialized] // 反序列化后处理 - private void OnDeserialized(StreamingContext context) - { - // 初始化默认值或执行验证 - Items ??= new Dictionary(); - } -} -``` - -### 🏭 模块设计模式 - -#### 1. 单一职责模块 - -```csharp -// 好的做法:模块职责单一 -public class AudioModule : AbstractModule -{ - // 只负责音频相关功能 -} - -public class SaveModule : AbstractModule -{ - // 只负责存档相关功能 -} - -// 避免:功能过于庞大 -public class GameModule : AbstractModule -{ - // 音频、存档、UI、输入全部混在一起 -} -``` - -#### 2. 模块间通信 - -```csharp -public class SaveModule : AbstractModule -{ - public override void Install(IArchitecture architecture) - { - architecture.RegisterUtility(new SaveUtility()); - } -} - -public class PlayerModule : AbstractModule -{ - public override void Install(IArchitecture architecture) - { - architecture.RegisterSystem(new PlayerSystem()); - } - - public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - if (phase == ArchitecturePhase.Ready) - { - // 通过事件进行模块间通信 - this.RegisterEvent(OnPlayerDeath); - } - } - - private void OnPlayerDeath(PlayerDeathEvent e) - { - // 触发保存模块的事件 - Context.SendEvent(new RequestAutoSaveEvent { Reason = "Player Death" }); - } -} -``` - -## 性能特性 - -### 📊 内存管理 - -- **缓存策略**:多层缓存减少磁盘 I/O -- **延迟加载**:按需加载资源,减少内存占用 -- **对象池化**:重用对象,减少 GC 压力 -- **内存映射**:大文件使用内存映射技术 - -### ⚡ 存储性能 - -```csharp -// 性能对比示例 -public class StoragePerformanceTest -{ - // 同步操作:简单直接 - public void SyncWrite(IStorage storage, string key, object data) - { - storage.Write(key, data); // ~1-5ms - } - - // 异步操作:非阻塞 - public async Task AsyncWrite(IStorage storage, string key, object data) - { - await storage.WriteAsync(key, data); // ~0.1-1ms (不阻塞主线程) - } - - // 批量操作:高吞吐量 - public async Task BatchWrite(IStorage storage, Dictionary data) - { - var tasks = data.Select(kvp => storage.WriteAsync(kvp.Key, kvp.Value)); - await Task.WhenAll(tasks); // ~10-50ms for 100 items - } -} -``` - -### 🔄 序列化优化 - -- **格式选择**:JSON(可读性)vs 二进制(性能) -- **压缩技术**:减少存储空间和网络传输 -- **增量序列化**:只序列化变化的部分 -- **版本控制**:向后兼容的数据迁移 - ---- +- **Settings** - 游戏设置系统,支持设置分类和配置应用 ## 依赖关系 -```mermaid -graph TD - A[GFramework.Game] --> B[GFramework.Core] - A --> C[GFramework.Core.Abstractions] - A --> D[Newtonsoft.Json] - A --> E[System.Text.Json] -``` - -## 版本兼容性 - -- **.NET**: 6.0+ -- **Newtonsoft.Json**: 13.0.3+ -- **GFramework.Core**: 与 Core 模块版本保持同步 - -## 许可证 - -本项目基于 Apache 2.0 许可证 - 详情请参阅 [LICENSE](../LICENSE) 文件。 +- 依赖 GFramework.Core +- 依赖 GFramework.Core.Abstractions ---- +## 详细文档 -**版本**: 1.0.0 -**更新日期**: 2026-01-12 \ No newline at end of file +参见 [docs/game/](../docs/game/) 目录下的详细文档。 diff --git a/GFramework.Godot.SourceGenerators/README.md b/GFramework.Godot.SourceGenerators/README.md index 4d77fc69..6da7cb39 100644 --- a/GFramework.Godot.SourceGenerators/README.md +++ b/GFramework.Godot.SourceGenerators/README.md @@ -1,14 +1,13 @@ # GFramework.Godot.SourceGenerators -`GFramework.Godot.SourceGenerators` 提供面向 Godot 场景的源码生成扩展,减少模板代码与手写样板。 +面向 Godot 场景的源码生成扩展模块,减少模板代码。 -## 主要内容 +## 主要功能 - 与 Godot 场景相关的编译期生成能力 - 基于 Roslyn 的增量生成器实现 -- 与 `GFramework.SourceGenerators` 协同工作 ## 使用建议 -- 仅在 **Godot + C#** 项目中启用。 -- 非 Godot 项目可只使用 `GeWuYou.GFramework.SourceGenerators`。 +- 仅在 Godot + C# 项目中启用 +- 非 Godot 项目可只使用 GFramework.SourceGenerators diff --git a/GFramework.Godot/README.md b/GFramework.Godot/README.md index d776d5fa..4f2bd472 100644 --- a/GFramework.Godot/README.md +++ b/GFramework.Godot/README.md @@ -1,908 +1,19 @@ # GFramework.Godot -> Godot 引擎深度集成 - 为 GFramework 框架提供原生的 Godot 支持 +GFramework 框架的 Godot 引擎集成模块,提供Godot特定的功能和扩展。 -GFramework.Godot 是 GFramework 框架的 Godot 特定实现,将框架的架构优势与 Godot 引擎的强大功能完美结合。 +## 主要功能 -## 📋 目录 - -- [概述](#概述) -- [核心特性](#核心特性) -- [架构集成](#架构集成) -- [Node 扩展方法](#node-扩展方法) -- [信号系统](#信号系统) -- [节点池化](#节点池化) -- [资源管理](#资源管理) -- [日志系统](#日志系统) -- [完整示例](#完整示例) -- [最佳实践](#最佳实践) -- [性能特性](#性能特性) - -## 概述 - -GFramework.Godot 提供了与 Godot 引擎的深度集成,让开发者能够在保持 GFramework 架构优势的同时,充分利用 Godot -的节点系统、信号机制和场景管理功能。 - -### 核心设计理念 - -- **无缝集成**:框架生命周期与 Godot 节点生命周期自动同步 -- **类型安全**:保持 GFramework 的强类型特性 -- **性能优化**:零额外开销的 Godot 集成 -- **开发效率**:丰富的扩展方法简化常见操作 - -## 核心特性 - -### 🎯 架构生命周期绑定 - -- 自动将框架初始化与 Godot 场景树绑定 -- 支持节点销毁时的自动清理 -- 阶段式架构初始化与 Godot `_Ready` 周期同步 - -### 🔧 丰富的 Node 扩展方法 - -- **50+** 个实用扩展方法 -- 安全的节点操作和验证 -- 流畅的场景树遍历和查找 -- 简化的输入处理 - -### 📡 流畅的信号 API - -- 类型安全的信号连接 -- 链式调用支持 -- 自动生命周期管理 -- Godot 信号与框架事件系统的桥接 - -### 🏊‍♂️ 高效的节点池化 - -- 专用的 Node 对象池 -- 自动回收和重用机制 -- 内存友好的高频节点创建/销毁 - -### 📦 智能资源管理 - -- 简化的 Godot 资源加载 -- 类型安全的资源工厂 -- 缓存和预加载支持 - -### 📝 Godot 原生日志 - -- 与 Godot 日志系统完全集成 -- 框架日志自动输出到 Godot 控制台 -- 可配置的日志级别 - -## 架构集成 - -### Architecture 基类 - -```csharp -using GFramework.Godot.architecture; - -public class GameArchitecture : AbstractArchitecture -{ - protected override void Init() - { - // 注册核心模型 - RegisterModel(new PlayerModel()); - RegisterModel(new GameModel()); - - // 注册系统 - RegisterSystem(new CombatSystem()); - RegisterSystem(new AudioSystem()); - - // 注册工具类 - RegisterUtility(new StorageUtility()); - RegisterUtility(new ResourceLoadUtility()); - } - - protected override void InstallModules() - { - // 安装 Godot 特定模块 - InstallGodotModule(new InputModule()); - InstallGodotModule(new AudioModule()); - } -} -``` - -### Godot 模块系统 - -```csharp -using GFramework.Godot.architecture; - -[ContextAware] -[Log] -public partial class AudioModule : AbstractGodotModule -{ - // 模块节点本身可以作为 Godot 节点 - public override Node Node => this; - - public override void Install(IArchitecture architecture) - { - // 注册音频相关系统 - architecture.RegisterSystem(new AudioSystem()); - architecture.RegisterUtility(new AudioUtility()); - } - - public override void OnAttach(Architecture architecture) - { - // 模块附加时的初始化 - Logger.Info("Audio module attached to architecture"); - } - - public override void OnDetach(Architecture architecture) - { - // 模块分离时的清理 - Logger.Info("Audio module detached from architecture"); - } - - // 响应架构生命周期阶段 - public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - switch (phase) - { - case ArchitecturePhase.Ready: - // 架构准备就绪,可以开始播放背景音乐 - PlayBackgroundMusic(); - break; - } - } -} -``` - -### Controller 集成 - -```csharp -using GFramework.Godot.extensions; - -[ContextAware] -[Log] -public partial class PlayerController : Node, IController -{ - private PlayerModel _playerModel; - - public override void _Ready() - { - // 获取模型引用 - _playerModel = Context.GetModel(); - - // 注册事件监听,自动与节点生命周期绑定 - this.RegisterEvent(OnPlayerInput) - .UnRegisterWhenNodeExitTree(this); - - // 监听属性变化 - _playerModel.Health.Register(OnHealthChanged) - .UnRegisterWhenNodeExitTree(this); - } - - private void OnPlayerInput(PlayerInputEvent e) - { - // 处理玩家输入 - switch (e.Action) - { - case "move_left": - MovePlayer(-1, 0); - break; - case "move_right": - MovePlayer(1, 0); - break; - case "attack": - Context.SendCommand(new AttackCommand()); - break; - } - } - - private void OnHealthChanged(int newHealth) - { - // 更新 UI - var healthBar = GetNode("UI/HealthBar"); - healthBar.Value = newHealth; - - // 播放音效 - if (newHealth < _playerModel.PreviousHealth) - PlayHurtSound(); - } -} -``` - -## Node 扩展方法 - -GFramework.Godot 提供了 50+ 个 Node 扩展方法,大大简化了 Godot 开发中的常见操作。 - -### 🔍 节点查找与验证 - -```csharp -// 安全的节点获取 -var player = GetNodeX("Player"); // 自动 null 检查和类型转换 -var child = FindChildX("Player"); // 递归查找子节点 - -// 节点验证 -if (IsValidNode(player)) -{ - // 节点有效且在场景树中 -} - -// 安全的节点遍历 -this.ForEachChild(child => { - GD.Print($"Found child: {child.Name}"); -}); -``` - -### 🌊 流畅的场景树操作 - -```csharp -// 安全的添加子节点 -var bullet = bulletScene.Instantiate(); -AddChildX(bullet); - -// 等待节点准备就绪 -await bullet.WaitUntilReady(); - -// 获取父节点 -var parent = GetParentX(); - -// 安全的节点移除 -bullet.QueueFreeX(); // 等效于 QueueFree() 但带有验证 -bullet.FreeX(); // 立即释放(谨慎使用) -``` - -### 🎮 输入处理简化 - -```csharp -// 输入处理 -SetInputAsHandled(); // 标记输入已处理 -DisableInput(); // 禁用输入 -EnableInput(); // 启用输入 - -// 输入状态检查 -if (Input.IsActionJustPressed("jump")) -{ - Jump(); -} -``` - -### 🔄 异步操作支持 - -```csharp -// 等待信号 -await ToSignal(this, SignalName.Ready); - -// 等待条件满足 -await WaitUntil(() => IsReady); - -// 等待帧结束 -await WaitUntilProcessFrame(); - -// 延迟执行 -await WaitUntilTimeout(2.0f); -``` - -## 信号系统 - -### SignalBuilder 流畅 API - -```csharp -using GFramework.Godot.extensions; - -// 基础信号连接 -this.ConnectSignal(Button.SignalName.Pressed, OnButtonPressed); - -// 流畅的信号构建 -this.CreateSignalBuilder(Timer.SignalName.Timeout) - .WithFlags(ConnectFlags.OneShot) // 单次触发 - .CallImmediately() // 立即调用一次 - .Connect(OnTimerTimeout) - .UnRegisterWhenNodeExitTree(this); - -// 多信号连接 -this.CreateSignalBuilder() - .AddSignal(Button.SignalName.Pressed, OnButtonPressed) - .AddSignal(Button.SignalName.MouseEntered, OnButtonHover) - .AddSignal(Button.SignalName.MouseExited, OnButtonExit) - .UnRegisterWhenNodeExitTree(this); -``` - -### 信号与框架事件桥接 - -```csharp -[ContextAware] -[Log] -public partial class UIController : Node, IController -{ - public override void _Ready() - { - // Godot 信号 -> 框架事件 - this.CreateSignalBuilder(Button.SignalName.Pressed) - .Connect(() => { - Context.SendEvent(new UIButtonClickEvent { ButtonId = "start_game" }); - }) - .UnRegisterWhenNodeExitTree(this); - - // 框架事件 -> Godot 信号 - this.RegisterEvent(OnHealthChanged) - .UnRegisterWhenNodeExitTree(this); - } - - private void OnHealthChanged(HealthChangeEvent e) - { - // 更新 Godot UI - var healthBar = GetNode("HealthBar"); - healthBar.Value = e.NewHealth; - - // 发送 Godot 信号 - EmitSignal(SignalName.HealthUpdated, e.NewHealth); - } - - [Signal] - public delegate void HealthUpdatedEventHandler(int newHealth); -} -``` - -## 节点池化 - -### AbstractNodePoolSystem 使用 - -```csharp -using GFramework.Godot.pool; - -public class BulletPoolSystem : AbstractNodePoolSystem -{ - private PackedScene _bulletScene; - - public BulletPoolSystem() - { - _bulletScene = GD.Load("res://scenes/Bullet.tscn"); - } - - protected override Bullet CreateItem(string key) - { - return _bulletScene.Instantiate(); - } - - protected override void OnSpawn(Bullet item, string key) - { - // 重置子弹状态 - item.Reset(); - item.Position = Vector3.Zero; - item.Visible = true; - } - - protected override void OnDespawn(Bullet item) - { - // 隐藏子弹 - item.Visible = false; - // 移除父节点 - item.GetParent()?.RemoveChild(item); - } - - protected override bool CanDespawn(Bullet item) - { - // 只有不在使用中的子弹才能回收 - return !item.IsActive; - } -} -``` - -### 池化系统使用 - -```csharp -[ContextAware] -[Log] -public partial class WeaponController : Node, IController -{ - private BulletPoolSystem _bulletPool; - - protected override void OnInit() - { - _bulletPool = Context.GetSystem(); - } - - public void Shoot(Vector3 direction) - { - // 从池中获取子弹 - var bullet = _bulletPool.Spawn("standard"); - - if (bullet != null) - { - // 设置子弹参数 - bullet.Direction = direction; - bullet.Speed = 10.0f; - - // 添加到场景 - GetTree().Root.AddChild(bullet); - - // 注册碰撞检测 - this.RegisterEvent(e => { - if (e.Bullet == bullet) - { - // 回收子弹 - _bulletPool.Despawn(bullet); - } - }).UnRegisterWhenNodeExitTree(this); - } - } -} -``` - -## 资源管理 - -### ResourceLoadUtility 使用 - -```csharp -using GFramework.Godot.assets; - -[ContextAware] -[Log] -public partial class ResourceManager : Node, IController -{ - private ResourceLoadUtility _resourceLoader; - - protected override void OnInit() - { - _resourceLoader = new ResourceLoadUtility(); - } - - public T LoadResource(string path) where T : Resource - { - return _resourceLoader.LoadResource(path); - } - - public async Task LoadResourceAsync(string path) where T : Resource - { - return await _resourceLoader.LoadResourceAsync(path); - } - - public void PreloadResources() - { - // 预加载常用资源 - _resourceLoader.PreloadResource("res://textures/player.png"); - _resourceLoader.PreloadResource("res://audio/shoot.wav"); - _resourceLoader.PreloadResource("res://scenes/enemy.tscn"); - } -} -``` - -### 自定义资源工厂 - -```csharp -public class GameResourceFactory : AbstractResourceFactoryUtility -{ - protected override void RegisterFactories() - { - RegisterFactory(CreatePlayerData); - RegisterFactory(CreateWeaponConfig); - RegisterFactory(CreateLevelData); - } - - private PlayerData CreatePlayerData(string path) - { - var config = LoadJson(path); - return new PlayerData - { - MaxHealth = config.MaxHealth, - Speed = config.Speed, - JumpForce = config.JumpForce - }; - } - - private WeaponConfig CreateWeaponConfig(string path) - { - var data = LoadJson(path); - return new WeaponConfig - { - Damage = data.Damage, - FireRate = data.FireRate, - BulletPrefab = LoadResource(data.BulletPath) - }; - } -} -``` - -## 日志系统 - -### GodotLogger 使用 - -```csharp -using GFramework.Godot.logging; - -[ContextAware] -[Log] // 自动生成 Logger 字段 -public partial class GameController : Node, IController -{ - public override void _Ready() - { - // 使用自动生成的 Logger - Logger.Info("Game controller ready"); - - try - { - InitializeGame(); - Logger.Info("Game initialized successfully"); - } - catch (Exception ex) - { - Logger.Error($"Failed to initialize game: {ex.Message}"); - } - } - - public void StartGame() - { - Logger.Debug("Starting game"); - - // 发送游戏开始事件 - Context.SendEvent(new GameStartEvent()); - - Logger.Info("Game started"); - } - - public void PauseGame() - { - Logger.Info("Game paused"); - Context.SendEvent(new GamePauseEvent()); - } -} -``` - -### 日志配置 - -```csharp -public class GodotLoggerFactoryProvider : ILoggerFactoryProvider -{ - public ILoggerFactory CreateFactory() - { - return new GodotLoggerFactory(new LoggerProperties - { - MinLevel = LogLevel.Debug, - IncludeTimestamp = true, - IncludeCallerInfo = true - }); - } -} - -// 在架构初始化时配置日志 -public class GameArchitecture : AbstractArchitecture -{ - protected override void Init() - { - // 配置 Godot 日志工厂 - LoggerProperties = new LoggerProperties - { - LoggerFactoryProvider = new GodotLoggerFactoryProvider(), - MinLevel = LogLevel.Info - }; - - // 注册组件... - } -} -``` - -## 完整示例 - -### 简单射击游戏示例 - -```csharp -// 1. 定义架构 -public class ShooterGameArchitecture : AbstractArchitecture -{ - protected override void Init() - { - // 注册模型 - RegisterModel(new PlayerModel()); - RegisterModel(new GameModel()); - RegisterModel(new ScoreModel()); - - // 注册系统 - RegisterSystem(new PlayerControllerSystem()); - RegisterSystem(new BulletPoolSystem()); - RegisterSystem(new EnemySpawnSystem()); - RegisterSystem(new CollisionSystem()); - - // 注册工具 - RegisterUtility(new StorageUtility()); - RegisterUtility(new ResourceLoadUtility()); - } -} - -// 2. 玩家控制器 -[ContextAware] -[Log] -public partial class PlayerController : CharacterBody2D, IController -{ - private PlayerModel _playerModel; - - public override void _Ready() - { - _playerModel = Context.GetModel(); - - // 输入处理 - SetProcessInput(true); - - // 注册事件 - this.RegisterEvent(OnDamage) - .UnRegisterWhenNodeExitTree(this); - } - - public override void _Process(double delta) - { - var inputDir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down"); - Velocity = inputDir * _playerModel.Speed.Value; - MoveAndSlide(); - } - - public override void _Input(InputEvent @event) - { - if (@event.IsActionPressed("shoot")) - { - Shoot(); - } - } - - private void Shoot() - { - if (CanShoot()) - { - var bulletPool = Context.GetSystem(); - var bullet = bulletPool.Spawn("player"); - - if (bullet != null) - { - var direction = GetGlobalMousePosition() - GlobalPosition; - bullet.Initialize(GlobalPosition, direction.Normalized()); - GetTree().Root.AddChild(bullet); - - Context.SendEvent(new BulletFiredEvent()); - } - } - } - - private void OnDamage(PlayerDamageEvent e) - { - _playerModel.Health.Value -= e.Damage; - - if (_playerModel.Health.Value <= 0) - { - Die(); - } - } - - private void Die() - { - Logger.Info("Player died"); - Context.SendEvent(new PlayerDeathEvent()); - QueueFreeX(); - } -} - -// 3. 主场景 -[ContextAware] -[Log] -public partial class MainScene : Node2D -{ - private ShooterGameArchitecture _architecture; - - public override void _Ready() - { - // 初始化架构 - _architecture = new ShooterGameArchitecture(); - _architecture.Initialize(); - - // 创建玩家 - var playerScene = GD.Load("res://scenes/Player.tscn"); - var player = playerScene.Instantiate(); - AddChild(player); - - // 注册全局事件 - this.RegisterEvent(OnPlayerDeath) - .UnRegisterWhenNodeExitTree(this); - - this.RegisterEvent(OnGameWin) - .UnRegisterWhenNodeExitTree(this); - - Logger.Info("Game started"); - } - - private void OnPlayerDeath(PlayerDeathEvent e) - { - Logger.Info("Game over"); - ShowGameOverScreen(); - } - - private void OnGameWin(GameWinEvent e) - { - Logger.Info("Victory!"); - ShowVictoryScreen(); - } - - private void ShowGameOverScreen() - { - var gameOverScene = GD.Load("res://ui/GameOver.tscn"); - var gameOverUI = gameOverScene.Instantiate(); - AddChild(gameOverUI); - } - - private void ShowVictoryScreen() - { - var victoryScene = GD.Load("res://ui/Victory.tscn"); - var victoryUI = victoryScene.Instantiate(); - AddChild(victoryUI); - } -} -``` - -## 最佳实践 - -### 🏗️ 架构设计最佳实践 - -#### 1. 模块化设计 - -```csharp -// 好的做法:按功能分组模块 -public class AudioModule : AbstractGodotModule { } -public class InputModule : AbstractGodotModule { } -public class UIModule : AbstractGodotModule { } - -// 避免的功能过于庞大的单一模块 -public class GameModule : AbstractGodotModule // ❌ 太大 -{ - // 音频、输入、UI、逻辑全部混在一起 -} -``` - -#### 2. 生命周期管理 - -```csharp -// 好的做法:使用自动清理 -this.RegisterEvent(OnGameEvent) - .UnRegisterWhenNodeExitTree(this); - -model.Property.Register(OnPropertyChange) - .UnRegisterWhenNodeExitTree(this); - -// 避免手动管理清理 -private IUnRegister _eventRegister; -public override void _Ready() -{ - _eventRegister = this.RegisterEvent(OnGameEvent); -} - -public override void _ExitTree() -{ - _eventRegister?.UnRegister(); // 容易忘记 -} -``` - -### 🎮 Godot 集成最佳实践 - -#### 1. 节点安全操作 - -```csharp -// 好的做法:使用安全扩展 -var player = GetNodeX("Player"); -var child = FindChildX("HealthBar"); - -// 避免的直接节点访问 -var player = GetNode("Player"); // 可能抛出异常 -``` - -#### 2. 信号连接模式 - -```csharp -// 好的做法:使用 SignalBuilder -this.CreateSignalBuilder(Button.SignalName.Pressed) - .UnRegisterWhenNodeExitTree(this) - .Connect(OnButtonPressed); - -// 避免的原始方式 -Button.Pressed += OnButtonPressed; // 容易忘记清理 -``` - -### 🏊‍♂️ 性能优化最佳实践 - -#### 1. 节点池化策略 - -```csharp -// 好的做法:高频创建对象使用池化 -public class BulletPool : AbstractNodePoolSystem -{ - // 为不同类型的子弹创建不同的池 -} - -// 避免的频繁创建销毁 -public void Shoot() -{ - var bullet = bulletScene.Instantiate(); // ❌ 性能问题 - // ... - bullet.QueueFree(); -} -``` - -#### 2. 资源预加载 - -```csharp -// 好的做法:预加载常用资源 -public override void _Ready() -{ - var resourceLoader = new ResourceLoadUtility(); - resourceLoader.PreloadResource("res://textures/bullet.png"); - resourceLoader.PreloadResource("res://audio/shoot.wav"); -} -``` - -### 🔧 调试和错误处理 - -#### 1. 日志使用策略 - -```csharp -// 好的做法:分级别记录 -Logger.Debug($"Player position: {Position}"); // 调试信息 -Logger.Info("Game started"); // 重要状态 -Logger.Warning($"Low health: {_playerModel.Health}"); // 警告 -Logger.Error($"Failed to load resource: {path}"); // 错误 - -// 避免的过度日志 -Logger.Debug($"Frame: {Engine.GetProcessFrames()}"); // 太频繁 -``` - -#### 2. 异常处理 - -```csharp -// 好的做法:优雅的错误处理 -public T LoadResource(string path) where T : Resource -{ - try - { - return GD.Load(path); - } - catch (Exception ex) - { - Logger.Error($"Failed to load resource {path}: {ex.Message}"); - return GetDefaultResource(); - } -} -``` - -## 性能特性 - -### 📊 内存管理 - -- **节点池化**:减少 GC 压力,提高频繁创建/销毁对象的性能 -- **资源缓存**:自动缓存已加载的 Godot 资源 -- **生命周期管理**:自动清理事件监听器和资源引用 - -### ⚡ 运行时性能 - -- **零分配**:扩展方法避免不必要的对象分配 -- **编译时优化**:Source Generators 减少运行时开销 -- **类型安全**:编译时类型检查,避免运行时错误 - -### 🔄 异步支持 - -- **信号等待**:使用 `await ToSignal()` 简化异步代码 -- **条件等待**:`WaitUntil()` 和 `WaitUntilTimeout()` 简化异步逻辑 -- **场景树等待**:`WaitUntilReady()` 确保节点准备就绪 - ---- +- **Extensions** - Godot节点扩展方法,简化常见开发任务 +- **Signal** - 流畅的信号连接API,支持链式调用 +- **Storage** - Godot文件存储系统,支持虚拟路径 +- **Settings** - Godot设置系统,管理音频和图形设置 ## 依赖关系 -```mermaid -graph TD - A[GFramework.Godot] --> B[GFramework.Game] - A --> C[GFramework.Game.Abstractions] - A --> D[GFramework.Core.Abstractions] - A --> E[Godot.SourceGenerators] - A --> F[GodotSharpEditor] -``` - -## 版本兼容性 - -- **Godot**: 4.5.1+ -- **.NET**: 6.0+ -- **GFramework.Core**: 与 Core 模块版本保持同步 - -## 许可证 - -本项目基于 Apache 2.0 许可证 - 详情请参阅 [LICENSE](../LICENSE) 文件。 +- 依赖 GFramework.Core +- 依赖 GFramework.Core.Abstractions ---- +## 详细文档 -**版本**: 1.0.0 -**更新日期**: 2026-01-12 \ No newline at end of file +参见 [docs/godot/](../docs/godot/) 目录下的详细文档。 diff --git a/docfx/docfx.json b/docfx/docfx.json deleted file mode 100644 index 453829ef..00000000 --- a/docfx/docfx.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "metadata": [ - { - "src": [ - { - "src": "..", - "files": [ - "GFramework.Core/GFramework.Core.csproj", - "GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj", - "GFramework.Game/GFramework.Game.csproj", - "GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj", - "GFramework.Godot/GFramework.Godot.csproj" - ] - } - ], - "dest": "api", - "filter": "filterConfig.yml", - "properties": { - "TargetFramework": "net8.0" - } - } - ], - "build": { - "content": [ - { "files": [ "index.md" ] }, - { "files": [ "api/**.yml" ] }, - { - "files": [ "**/*.md", "**/toc.yml" ], - "src": "../docs" - } - ], - "resource": [ - { "files": [ "images/**", "**/*.png", "**/*.jpg", "**/*.jpeg", "**/*.gif", "**/*.svg" ] } - ], - "dest": "_site", - "template": [ "default", "templates/material" ], - "globalMetadata": { - "_appTitle": "GFramework Documentation", - "_enableSearch": true, - "_appFooter": "Copyright © 2026 GeWuYou. All rights reserved.", - "pdf": false - }, - "xref": [ - "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json" - ] - } -} \ No newline at end of file diff --git a/docfx/filterConfig.yml b/docfx/filterConfig.yml deleted file mode 100644 index ff8587ba..00000000 --- a/docfx/filterConfig.yml +++ /dev/null @@ -1,13 +0,0 @@ -apiRules: -- exclude: - # 排除测试项目 - uidRegex: ^.*Tests$ - type: Namespace -- exclude: - # 排除生成器项目 - uidRegex: ^.*SourceGenerators.*$ - type: Namespace -- exclude: - # 排除内部实现细节 - uidRegex: ^.*Internals.*$ - type: Namespace \ No newline at end of file diff --git a/docfx/index.md b/docfx/index.md deleted file mode 100644 index 6f73b466..00000000 --- a/docfx/index.md +++ /dev/null @@ -1,6 +0,0 @@ -# GFramework Documentation - -欢迎来到 **GFramework** 文档站点。 - -- 📘 [使用指南](../docs/index.md) -- 🧩 [API 文档](_site/api/index.html) diff --git a/docfx/toc.yml b/docfx/toc.yml deleted file mode 100644 index 276b2c52..00000000 --- a/docfx/toc.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: Home - href: ../docs/index.md -- name: API Documentation - href: api/ - homepage: api/namespaces.md \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..f702dc2e --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.vitepress/dist/ \ No newline at end of file diff --git a/docs/.vitepress/cache/deps/_metadata.json b/docs/.vitepress/cache/deps/_metadata.json new file mode 100644 index 00000000..44658bc0 --- /dev/null +++ b/docs/.vitepress/cache/deps/_metadata.json @@ -0,0 +1,31 @@ +{ + "hash": "74ecdc37", + "configHash": "1c302118", + "lockfileHash": "42b6a898", + "browserHash": "b3e735e5", + "optimized": { + "vue": { + "src": "../../../node_modules/vue/dist/vue.runtime.esm-bundler.js", + "file": "vue.js", + "fileHash": "5c6400c6", + "needsInterop": false + }, + "vitepress > @vue/devtools-api": { + "src": "../../../node_modules/@vue/devtools-api/dist/index.js", + "file": "vitepress___@vue_devtools-api.js", + "fileHash": "673694b5", + "needsInterop": false + }, + "vitepress > @vueuse/core": { + "src": "../../../node_modules/@vueuse/core/dist/index.js", + "file": "vitepress___@vueuse_core.js", + "fileHash": "7fde5c45", + "needsInterop": false + } + }, + "chunks": { + "chunk-FOJXB67H": { + "file": "chunk-FOJXB67H.js" + } + } +} \ No newline at end of file diff --git a/docs/.vitepress/cache/deps/chunk-FOJXB67H.js b/docs/.vitepress/cache/deps/chunk-FOJXB67H.js new file mode 100644 index 00000000..f42a9a6b --- /dev/null +++ b/docs/.vitepress/cache/deps/chunk-FOJXB67H.js @@ -0,0 +1,12828 @@ +// node_modules/@vue/shared/dist/shared.esm-bundler.js +function makeMap(str) { + const map2 = /* @__PURE__ */ Object.create(null); + for (const key of str.split(",")) map2[key] = 1; + return (val) => val in map2; +} +var EMPTY_OBJ = true ? Object.freeze({}) : {}; +var EMPTY_ARR = true ? Object.freeze([]) : []; +var NOOP = () => { +}; +var NO = () => false; +var isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter +(key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97); +var isModelListener = (key) => key.startsWith("onUpdate:"); +var extend = Object.assign; +var remove = (arr, el) => { + const i = arr.indexOf(el); + if (i > -1) { + arr.splice(i, 1); + } +}; +var hasOwnProperty = Object.prototype.hasOwnProperty; +var hasOwn = (val, key) => hasOwnProperty.call(val, key); +var isArray = Array.isArray; +var isMap = (val) => toTypeString(val) === "[object Map]"; +var isSet = (val) => toTypeString(val) === "[object Set]"; +var isDate = (val) => toTypeString(val) === "[object Date]"; +var isRegExp = (val) => toTypeString(val) === "[object RegExp]"; +var isFunction = (val) => typeof val === "function"; +var isString = (val) => typeof val === "string"; +var isSymbol = (val) => typeof val === "symbol"; +var isObject = (val) => val !== null && typeof val === "object"; +var isPromise = (val) => { + return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch); +}; +var objectToString = Object.prototype.toString; +var toTypeString = (value) => objectToString.call(value); +var toRawType = (value) => { + return toTypeString(value).slice(8, -1); +}; +var isPlainObject = (val) => toTypeString(val) === "[object Object]"; +var isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key; +var isReservedProp = makeMap( + // the leading comma is intentional so empty string "" is also included + ",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted" +); +var isBuiltInDirective = makeMap( + "bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo" +); +var cacheStringFunction = (fn) => { + const cache = /* @__PURE__ */ Object.create(null); + return ((str) => { + const hit = cache[str]; + return hit || (cache[str] = fn(str)); + }); +}; +var camelizeRE = /-\w/g; +var camelize = cacheStringFunction( + (str) => { + return str.replace(camelizeRE, (c) => c.slice(1).toUpperCase()); + } +); +var hyphenateRE = /\B([A-Z])/g; +var hyphenate = cacheStringFunction( + (str) => str.replace(hyphenateRE, "-$1").toLowerCase() +); +var capitalize = cacheStringFunction((str) => { + return str.charAt(0).toUpperCase() + str.slice(1); +}); +var toHandlerKey = cacheStringFunction( + (str) => { + const s = str ? `on${capitalize(str)}` : ``; + return s; + } +); +var hasChanged = (value, oldValue) => !Object.is(value, oldValue); +var invokeArrayFns = (fns, ...arg) => { + for (let i = 0; i < fns.length; i++) { + fns[i](...arg); + } +}; +var def = (obj, key, value, writable = false) => { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + writable, + value + }); +}; +var looseToNumber = (val) => { + const n = parseFloat(val); + return isNaN(n) ? val : n; +}; +var toNumber = (val) => { + const n = isString(val) ? Number(val) : NaN; + return isNaN(n) ? val : n; +}; +var _globalThis; +var getGlobalThis = () => { + return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {}); +}; +var GLOBALS_ALLOWED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol"; +var isGloballyAllowed = makeMap(GLOBALS_ALLOWED); +function normalizeStyle(value) { + if (isArray(value)) { + const res = {}; + for (let i = 0; i < value.length; i++) { + const item = value[i]; + const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item); + if (normalized) { + for (const key in normalized) { + res[key] = normalized[key]; + } + } + } + return res; + } else if (isString(value) || isObject(value)) { + return value; + } +} +var listDelimiterRE = /;(?![^(]*\))/g; +var propertyDelimiterRE = /:([^]+)/; +var styleCommentRE = /\/\*[^]*?\*\//g; +function parseStringStyle(cssText) { + const ret = {}; + cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => { + if (item) { + const tmp = item.split(propertyDelimiterRE); + tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()); + } + }); + return ret; +} +function stringifyStyle(styles) { + if (!styles) return ""; + if (isString(styles)) return styles; + let ret = ""; + for (const key in styles) { + const value = styles[key]; + if (isString(value) || typeof value === "number") { + const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key); + ret += `${normalizedKey}:${value};`; + } + } + return ret; +} +function normalizeClass(value) { + let res = ""; + if (isString(value)) { + res = value; + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const normalized = normalizeClass(value[i]); + if (normalized) { + res += normalized + " "; + } + } + } else if (isObject(value)) { + for (const name in value) { + if (value[name]) { + res += name + " "; + } + } + } + return res.trim(); +} +function normalizeProps(props) { + if (!props) return null; + let { class: klass, style } = props; + if (klass && !isString(klass)) { + props.class = normalizeClass(klass); + } + if (style) { + props.style = normalizeStyle(style); + } + return props; +} +var HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot"; +var SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view"; +var MATH_TAGS = "annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics"; +var VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr"; +var isHTMLTag = makeMap(HTML_TAGS); +var isSVGTag = makeMap(SVG_TAGS); +var isMathMLTag = makeMap(MATH_TAGS); +var isVoidTag = makeMap(VOID_TAGS); +var specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`; +var isSpecialBooleanAttr = makeMap(specialBooleanAttrs); +var isBooleanAttr = makeMap( + specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected` +); +function includeBooleanAttr(value) { + return !!value || value === ""; +} +var isKnownHtmlAttr = makeMap( + `accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap` +); +var isKnownSvgAttr = makeMap( + `xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan` +); +var isKnownMathMLAttr = makeMap( + `accent,accentunder,actiontype,align,alignmentscope,altimg,altimg-height,altimg-valign,altimg-width,alttext,bevelled,close,columnsalign,columnlines,columnspan,denomalign,depth,dir,display,displaystyle,encoding,equalcolumns,equalrows,fence,fontstyle,fontweight,form,frame,framespacing,groupalign,height,href,id,indentalign,indentalignfirst,indentalignlast,indentshift,indentshiftfirst,indentshiftlast,indextype,justify,largetop,largeop,lquote,lspace,mathbackground,mathcolor,mathsize,mathvariant,maxsize,minlabelspacing,mode,other,overflow,position,rowalign,rowlines,rowspan,rquote,rspace,scriptlevel,scriptminsize,scriptsizemultiplier,selection,separator,separators,shift,side,src,stackalign,stretchy,subscriptshift,superscriptshift,symmetric,voffset,width,widths,xlink:href,xlink:show,xlink:type,xmlns` +); +function isRenderableAttrValue(value) { + if (value == null) { + return false; + } + const type = typeof value; + return type === "string" || type === "number" || type === "boolean"; +} +var cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g; +function getEscapedCssVarName(key, doubleEscape) { + return key.replace( + cssVarNameEscapeSymbolsRE, + (s) => doubleEscape ? s === '"' ? '\\\\\\"' : `\\\\${s}` : `\\${s}` + ); +} +function looseCompareArrays(a, b) { + if (a.length !== b.length) return false; + let equal = true; + for (let i = 0; equal && i < a.length; i++) { + equal = looseEqual(a[i], b[i]); + } + return equal; +} +function looseEqual(a, b) { + if (a === b) return true; + let aValidType = isDate(a); + let bValidType = isDate(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? a.getTime() === b.getTime() : false; + } + aValidType = isSymbol(a); + bValidType = isSymbol(b); + if (aValidType || bValidType) { + return a === b; + } + aValidType = isArray(a); + bValidType = isArray(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? looseCompareArrays(a, b) : false; + } + aValidType = isObject(a); + bValidType = isObject(b); + if (aValidType || bValidType) { + if (!aValidType || !bValidType) { + return false; + } + const aKeysCount = Object.keys(a).length; + const bKeysCount = Object.keys(b).length; + if (aKeysCount !== bKeysCount) { + return false; + } + for (const key in a) { + const aHasKey = a.hasOwnProperty(key); + const bHasKey = b.hasOwnProperty(key); + if (aHasKey && !bHasKey || !aHasKey && bHasKey || !looseEqual(a[key], b[key])) { + return false; + } + } + } + return String(a) === String(b); +} +function looseIndexOf(arr, val) { + return arr.findIndex((item) => looseEqual(item, val)); +} +var isRef = (val) => { + return !!(val && val["__v_isRef"] === true); +}; +var toDisplayString = (val) => { + return isString(val) ? val : val == null ? "" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? isRef(val) ? toDisplayString(val.value) : JSON.stringify(val, replacer, 2) : String(val); +}; +var replacer = (_key, val) => { + if (isRef(val)) { + return replacer(_key, val.value); + } else if (isMap(val)) { + return { + [`Map(${val.size})`]: [...val.entries()].reduce( + (entries, [key, val2], i) => { + entries[stringifySymbol(key, i) + " =>"] = val2; + return entries; + }, + {} + ) + }; + } else if (isSet(val)) { + return { + [`Set(${val.size})`]: [...val.values()].map((v) => stringifySymbol(v)) + }; + } else if (isSymbol(val)) { + return stringifySymbol(val); + } else if (isObject(val) && !isArray(val) && !isPlainObject(val)) { + return String(val); + } + return val; +}; +var stringifySymbol = (v, i = "") => { + var _a; + return ( + // Symbol.description in es2019+ so we need to cast here to pass + // the lib: es2016 check + isSymbol(v) ? `Symbol(${(_a = v.description) != null ? _a : i})` : v + ); +}; +function normalizeCssVarValue(value) { + if (value == null) { + return "initial"; + } + if (typeof value === "string") { + return value === "" ? " " : value; + } + if (typeof value !== "number" || !Number.isFinite(value)) { + if (true) { + console.warn( + "[Vue warn] Invalid value used for CSS binding. Expected a string or a finite number but received:", + value + ); + } + } + return String(value); +} + +// node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js +function warn(msg, ...args) { + console.warn(`[Vue warn] ${msg}`, ...args); +} +var activeEffectScope; +var EffectScope = class { + // TODO isolatedDeclarations "__v_skip" + constructor(detached = false) { + this.detached = detached; + this._active = true; + this._on = 0; + this.effects = []; + this.cleanups = []; + this._isPaused = false; + this.__v_skip = true; + this.parent = activeEffectScope; + if (!detached && activeEffectScope) { + this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( + this + ) - 1; + } + } + get active() { + return this._active; + } + pause() { + if (this._active) { + this._isPaused = true; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].pause(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].pause(); + } + } + } + /** + * Resumes the effect scope, including all child scopes and effects. + */ + resume() { + if (this._active) { + if (this._isPaused) { + this._isPaused = false; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].resume(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].resume(); + } + } + } + } + run(fn) { + if (this._active) { + const currentEffectScope = activeEffectScope; + try { + activeEffectScope = this; + return fn(); + } finally { + activeEffectScope = currentEffectScope; + } + } else if (true) { + warn(`cannot run an inactive effect scope.`); + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + on() { + if (++this._on === 1) { + this.prevScope = activeEffectScope; + activeEffectScope = this; + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + off() { + if (this._on > 0 && --this._on === 0) { + activeEffectScope = this.prevScope; + this.prevScope = void 0; + } + } + stop(fromParent) { + if (this._active) { + this._active = false; + let i, l; + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].stop(); + } + this.effects.length = 0; + for (i = 0, l = this.cleanups.length; i < l; i++) { + this.cleanups[i](); + } + this.cleanups.length = 0; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].stop(true); + } + this.scopes.length = 0; + } + if (!this.detached && this.parent && !fromParent) { + const last = this.parent.scopes.pop(); + if (last && last !== this) { + this.parent.scopes[this.index] = last; + last.index = this.index; + } + } + this.parent = void 0; + } + } +}; +function effectScope(detached) { + return new EffectScope(detached); +} +function getCurrentScope() { + return activeEffectScope; +} +function onScopeDispose(fn, failSilently = false) { + if (activeEffectScope) { + activeEffectScope.cleanups.push(fn); + } else if (!failSilently) { + warn( + `onScopeDispose() is called when there is no active effect scope to be associated with.` + ); + } +} +var activeSub; +var pausedQueueEffects = /* @__PURE__ */ new WeakSet(); +var ReactiveEffect = class { + constructor(fn) { + this.fn = fn; + this.deps = void 0; + this.depsTail = void 0; + this.flags = 1 | 4; + this.next = void 0; + this.cleanup = void 0; + this.scheduler = void 0; + if (activeEffectScope && activeEffectScope.active) { + activeEffectScope.effects.push(this); + } + } + pause() { + this.flags |= 64; + } + resume() { + if (this.flags & 64) { + this.flags &= -65; + if (pausedQueueEffects.has(this)) { + pausedQueueEffects.delete(this); + this.trigger(); + } + } + } + /** + * @internal + */ + notify() { + if (this.flags & 2 && !(this.flags & 32)) { + return; + } + if (!(this.flags & 8)) { + batch(this); + } + } + run() { + if (!(this.flags & 1)) { + return this.fn(); + } + this.flags |= 2; + cleanupEffect(this); + prepareDeps(this); + const prevEffect = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = this; + shouldTrack = true; + try { + return this.fn(); + } finally { + if (activeSub !== this) { + warn( + "Active effect was not restored correctly - this is likely a Vue internal bug." + ); + } + cleanupDeps(this); + activeSub = prevEffect; + shouldTrack = prevShouldTrack; + this.flags &= -3; + } + } + stop() { + if (this.flags & 1) { + for (let link = this.deps; link; link = link.nextDep) { + removeSub(link); + } + this.deps = this.depsTail = void 0; + cleanupEffect(this); + this.onStop && this.onStop(); + this.flags &= -2; + } + } + trigger() { + if (this.flags & 64) { + pausedQueueEffects.add(this); + } else if (this.scheduler) { + this.scheduler(); + } else { + this.runIfDirty(); + } + } + /** + * @internal + */ + runIfDirty() { + if (isDirty(this)) { + this.run(); + } + } + get dirty() { + return isDirty(this); + } +}; +var batchDepth = 0; +var batchedSub; +var batchedComputed; +function batch(sub, isComputed = false) { + sub.flags |= 8; + if (isComputed) { + sub.next = batchedComputed; + batchedComputed = sub; + return; + } + sub.next = batchedSub; + batchedSub = sub; +} +function startBatch() { + batchDepth++; +} +function endBatch() { + if (--batchDepth > 0) { + return; + } + if (batchedComputed) { + let e = batchedComputed; + batchedComputed = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= -9; + e = next; + } + } + let error; + while (batchedSub) { + let e = batchedSub; + batchedSub = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= -9; + if (e.flags & 1) { + try { + ; + e.trigger(); + } catch (err) { + if (!error) error = err; + } + } + e = next; + } + } + if (error) throw error; +} +function prepareDeps(sub) { + for (let link = sub.deps; link; link = link.nextDep) { + link.version = -1; + link.prevActiveLink = link.dep.activeLink; + link.dep.activeLink = link; + } +} +function cleanupDeps(sub) { + let head; + let tail = sub.depsTail; + let link = tail; + while (link) { + const prev = link.prevDep; + if (link.version === -1) { + if (link === tail) tail = prev; + removeSub(link); + removeDep(link); + } else { + head = link; + } + link.dep.activeLink = link.prevActiveLink; + link.prevActiveLink = void 0; + link = prev; + } + sub.deps = head; + sub.depsTail = tail; +} +function isDirty(sub) { + for (let link = sub.deps; link; link = link.nextDep) { + if (link.dep.version !== link.version || link.dep.computed && (refreshComputed(link.dep.computed) || link.dep.version !== link.version)) { + return true; + } + } + if (sub._dirty) { + return true; + } + return false; +} +function refreshComputed(computed3) { + if (computed3.flags & 4 && !(computed3.flags & 16)) { + return; + } + computed3.flags &= -17; + if (computed3.globalVersion === globalVersion) { + return; + } + computed3.globalVersion = globalVersion; + if (!computed3.isSSR && computed3.flags & 128 && (!computed3.deps && !computed3._dirty || !isDirty(computed3))) { + return; + } + computed3.flags |= 2; + const dep = computed3.dep; + const prevSub = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = computed3; + shouldTrack = true; + try { + prepareDeps(computed3); + const value = computed3.fn(computed3._value); + if (dep.version === 0 || hasChanged(value, computed3._value)) { + computed3.flags |= 128; + computed3._value = value; + dep.version++; + } + } catch (err) { + dep.version++; + throw err; + } finally { + activeSub = prevSub; + shouldTrack = prevShouldTrack; + cleanupDeps(computed3); + computed3.flags &= -3; + } +} +function removeSub(link, soft = false) { + const { dep, prevSub, nextSub } = link; + if (prevSub) { + prevSub.nextSub = nextSub; + link.prevSub = void 0; + } + if (nextSub) { + nextSub.prevSub = prevSub; + link.nextSub = void 0; + } + if (dep.subsHead === link) { + dep.subsHead = nextSub; + } + if (dep.subs === link) { + dep.subs = prevSub; + if (!prevSub && dep.computed) { + dep.computed.flags &= -5; + for (let l = dep.computed.deps; l; l = l.nextDep) { + removeSub(l, true); + } + } + } + if (!soft && !--dep.sc && dep.map) { + dep.map.delete(dep.key); + } +} +function removeDep(link) { + const { prevDep, nextDep } = link; + if (prevDep) { + prevDep.nextDep = nextDep; + link.prevDep = void 0; + } + if (nextDep) { + nextDep.prevDep = prevDep; + link.nextDep = void 0; + } +} +function effect(fn, options) { + if (fn.effect instanceof ReactiveEffect) { + fn = fn.effect.fn; + } + const e = new ReactiveEffect(fn); + if (options) { + extend(e, options); + } + try { + e.run(); + } catch (err) { + e.stop(); + throw err; + } + const runner = e.run.bind(e); + runner.effect = e; + return runner; +} +function stop(runner) { + runner.effect.stop(); +} +var shouldTrack = true; +var trackStack = []; +function pauseTracking() { + trackStack.push(shouldTrack); + shouldTrack = false; +} +function resetTracking() { + const last = trackStack.pop(); + shouldTrack = last === void 0 ? true : last; +} +function cleanupEffect(e) { + const { cleanup } = e; + e.cleanup = void 0; + if (cleanup) { + const prevSub = activeSub; + activeSub = void 0; + try { + cleanup(); + } finally { + activeSub = prevSub; + } + } +} +var globalVersion = 0; +var Link = class { + constructor(sub, dep) { + this.sub = sub; + this.dep = dep; + this.version = dep.version; + this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = void 0; + } +}; +var Dep = class { + // TODO isolatedDeclarations "__v_skip" + constructor(computed3) { + this.computed = computed3; + this.version = 0; + this.activeLink = void 0; + this.subs = void 0; + this.map = void 0; + this.key = void 0; + this.sc = 0; + this.__v_skip = true; + if (true) { + this.subsHead = void 0; + } + } + track(debugInfo) { + if (!activeSub || !shouldTrack || activeSub === this.computed) { + return; + } + let link = this.activeLink; + if (link === void 0 || link.sub !== activeSub) { + link = this.activeLink = new Link(activeSub, this); + if (!activeSub.deps) { + activeSub.deps = activeSub.depsTail = link; + } else { + link.prevDep = activeSub.depsTail; + activeSub.depsTail.nextDep = link; + activeSub.depsTail = link; + } + addSub(link); + } else if (link.version === -1) { + link.version = this.version; + if (link.nextDep) { + const next = link.nextDep; + next.prevDep = link.prevDep; + if (link.prevDep) { + link.prevDep.nextDep = next; + } + link.prevDep = activeSub.depsTail; + link.nextDep = void 0; + activeSub.depsTail.nextDep = link; + activeSub.depsTail = link; + if (activeSub.deps === link) { + activeSub.deps = next; + } + } + } + if (activeSub.onTrack) { + activeSub.onTrack( + extend( + { + effect: activeSub + }, + debugInfo + ) + ); + } + return link; + } + trigger(debugInfo) { + this.version++; + globalVersion++; + this.notify(debugInfo); + } + notify(debugInfo) { + startBatch(); + try { + if (true) { + for (let head = this.subsHead; head; head = head.nextSub) { + if (head.sub.onTrigger && !(head.sub.flags & 8)) { + head.sub.onTrigger( + extend( + { + effect: head.sub + }, + debugInfo + ) + ); + } + } + } + for (let link = this.subs; link; link = link.prevSub) { + if (link.sub.notify()) { + ; + link.sub.dep.notify(); + } + } + } finally { + endBatch(); + } + } +}; +function addSub(link) { + link.dep.sc++; + if (link.sub.flags & 4) { + const computed3 = link.dep.computed; + if (computed3 && !link.dep.subs) { + computed3.flags |= 4 | 16; + for (let l = computed3.deps; l; l = l.nextDep) { + addSub(l); + } + } + const currentTail = link.dep.subs; + if (currentTail !== link) { + link.prevSub = currentTail; + if (currentTail) currentTail.nextSub = link; + } + if (link.dep.subsHead === void 0) { + link.dep.subsHead = link; + } + link.dep.subs = link; + } +} +var targetMap = /* @__PURE__ */ new WeakMap(); +var ITERATE_KEY = /* @__PURE__ */ Symbol( + true ? "Object iterate" : "" +); +var MAP_KEY_ITERATE_KEY = /* @__PURE__ */ Symbol( + true ? "Map keys iterate" : "" +); +var ARRAY_ITERATE_KEY = /* @__PURE__ */ Symbol( + true ? "Array iterate" : "" +); +function track(target, type, key) { + if (shouldTrack && activeSub) { + let depsMap = targetMap.get(target); + if (!depsMap) { + targetMap.set(target, depsMap = /* @__PURE__ */ new Map()); + } + let dep = depsMap.get(key); + if (!dep) { + depsMap.set(key, dep = new Dep()); + dep.map = depsMap; + dep.key = key; + } + if (true) { + dep.track({ + target, + type, + key + }); + } else { + dep.track(); + } + } +} +function trigger(target, type, key, newValue, oldValue, oldTarget) { + const depsMap = targetMap.get(target); + if (!depsMap) { + globalVersion++; + return; + } + const run = (dep) => { + if (dep) { + if (true) { + dep.trigger({ + target, + type, + key, + newValue, + oldValue, + oldTarget + }); + } else { + dep.trigger(); + } + } + }; + startBatch(); + if (type === "clear") { + depsMap.forEach(run); + } else { + const targetIsArray = isArray(target); + const isArrayIndex = targetIsArray && isIntegerKey(key); + if (targetIsArray && key === "length") { + const newLength = Number(newValue); + depsMap.forEach((dep, key2) => { + if (key2 === "length" || key2 === ARRAY_ITERATE_KEY || !isSymbol(key2) && key2 >= newLength) { + run(dep); + } + }); + } else { + if (key !== void 0 || depsMap.has(void 0)) { + run(depsMap.get(key)); + } + if (isArrayIndex) { + run(depsMap.get(ARRAY_ITERATE_KEY)); + } + switch (type) { + case "add": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } else if (isArrayIndex) { + run(depsMap.get("length")); + } + break; + case "delete": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } + break; + case "set": + if (isMap(target)) { + run(depsMap.get(ITERATE_KEY)); + } + break; + } + } + } + endBatch(); +} +function getDepFromReactive(object, key) { + const depMap = targetMap.get(object); + return depMap && depMap.get(key); +} +function reactiveReadArray(array) { + const raw = toRaw(array); + if (raw === array) return raw; + track(raw, "iterate", ARRAY_ITERATE_KEY); + return isShallow(array) ? raw : raw.map(toReactive); +} +function shallowReadArray(arr) { + track(arr = toRaw(arr), "iterate", ARRAY_ITERATE_KEY); + return arr; +} +function toWrapped(target, item) { + if (isReadonly(target)) { + return isReactive(target) ? toReadonly(toReactive(item)) : toReadonly(item); + } + return toReactive(item); +} +var arrayInstrumentations = { + __proto__: null, + [Symbol.iterator]() { + return iterator(this, Symbol.iterator, (item) => toWrapped(this, item)); + }, + concat(...args) { + return reactiveReadArray(this).concat( + ...args.map((x) => isArray(x) ? reactiveReadArray(x) : x) + ); + }, + entries() { + return iterator(this, "entries", (value) => { + value[1] = toWrapped(this, value[1]); + return value; + }); + }, + every(fn, thisArg) { + return apply(this, "every", fn, thisArg, void 0, arguments); + }, + filter(fn, thisArg) { + return apply( + this, + "filter", + fn, + thisArg, + (v) => v.map((item) => toWrapped(this, item)), + arguments + ); + }, + find(fn, thisArg) { + return apply( + this, + "find", + fn, + thisArg, + (item) => toWrapped(this, item), + arguments + ); + }, + findIndex(fn, thisArg) { + return apply(this, "findIndex", fn, thisArg, void 0, arguments); + }, + findLast(fn, thisArg) { + return apply( + this, + "findLast", + fn, + thisArg, + (item) => toWrapped(this, item), + arguments + ); + }, + findLastIndex(fn, thisArg) { + return apply(this, "findLastIndex", fn, thisArg, void 0, arguments); + }, + // flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement + forEach(fn, thisArg) { + return apply(this, "forEach", fn, thisArg, void 0, arguments); + }, + includes(...args) { + return searchProxy(this, "includes", args); + }, + indexOf(...args) { + return searchProxy(this, "indexOf", args); + }, + join(separator) { + return reactiveReadArray(this).join(separator); + }, + // keys() iterator only reads `length`, no optimization required + lastIndexOf(...args) { + return searchProxy(this, "lastIndexOf", args); + }, + map(fn, thisArg) { + return apply(this, "map", fn, thisArg, void 0, arguments); + }, + pop() { + return noTracking(this, "pop"); + }, + push(...args) { + return noTracking(this, "push", args); + }, + reduce(fn, ...args) { + return reduce(this, "reduce", fn, args); + }, + reduceRight(fn, ...args) { + return reduce(this, "reduceRight", fn, args); + }, + shift() { + return noTracking(this, "shift"); + }, + // slice could use ARRAY_ITERATE but also seems to beg for range tracking + some(fn, thisArg) { + return apply(this, "some", fn, thisArg, void 0, arguments); + }, + splice(...args) { + return noTracking(this, "splice", args); + }, + toReversed() { + return reactiveReadArray(this).toReversed(); + }, + toSorted(comparer) { + return reactiveReadArray(this).toSorted(comparer); + }, + toSpliced(...args) { + return reactiveReadArray(this).toSpliced(...args); + }, + unshift(...args) { + return noTracking(this, "unshift", args); + }, + values() { + return iterator(this, "values", (item) => toWrapped(this, item)); + } +}; +function iterator(self2, method, wrapValue) { + const arr = shallowReadArray(self2); + const iter = arr[method](); + if (arr !== self2 && !isShallow(self2)) { + iter._next = iter.next; + iter.next = () => { + const result = iter._next(); + if (!result.done) { + result.value = wrapValue(result.value); + } + return result; + }; + } + return iter; +} +var arrayProto = Array.prototype; +function apply(self2, method, fn, thisArg, wrappedRetFn, args) { + const arr = shallowReadArray(self2); + const needsWrap = arr !== self2 && !isShallow(self2); + const methodFn = arr[method]; + if (methodFn !== arrayProto[method]) { + const result2 = methodFn.apply(self2, args); + return needsWrap ? toReactive(result2) : result2; + } + let wrappedFn = fn; + if (arr !== self2) { + if (needsWrap) { + wrappedFn = function(item, index) { + return fn.call(this, toWrapped(self2, item), index, self2); + }; + } else if (fn.length > 2) { + wrappedFn = function(item, index) { + return fn.call(this, item, index, self2); + }; + } + } + const result = methodFn.call(arr, wrappedFn, thisArg); + return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result; +} +function reduce(self2, method, fn, args) { + const arr = shallowReadArray(self2); + let wrappedFn = fn; + if (arr !== self2) { + if (!isShallow(self2)) { + wrappedFn = function(acc, item, index) { + return fn.call(this, acc, toWrapped(self2, item), index, self2); + }; + } else if (fn.length > 3) { + wrappedFn = function(acc, item, index) { + return fn.call(this, acc, item, index, self2); + }; + } + } + return arr[method](wrappedFn, ...args); +} +function searchProxy(self2, method, args) { + const arr = toRaw(self2); + track(arr, "iterate", ARRAY_ITERATE_KEY); + const res = arr[method](...args); + if ((res === -1 || res === false) && isProxy(args[0])) { + args[0] = toRaw(args[0]); + return arr[method](...args); + } + return res; +} +function noTracking(self2, method, args = []) { + pauseTracking(); + startBatch(); + const res = toRaw(self2)[method].apply(self2, args); + endBatch(); + resetTracking(); + return res; +} +var isNonTrackableKeys = makeMap(`__proto__,__v_isRef,__isVue`); +var builtInSymbols = new Set( + Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(isSymbol) +); +function hasOwnProperty2(key) { + if (!isSymbol(key)) key = String(key); + const obj = toRaw(this); + track(obj, "has", key); + return obj.hasOwnProperty(key); +} +var BaseReactiveHandler = class { + constructor(_isReadonly = false, _isShallow = false) { + this._isReadonly = _isReadonly; + this._isShallow = _isShallow; + } + get(target, key, receiver) { + if (key === "__v_skip") return target["__v_skip"]; + const isReadonly2 = this._isReadonly, isShallow2 = this._isShallow; + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_isShallow") { + return isShallow2; + } else if (key === "__v_raw") { + if (receiver === (isReadonly2 ? isShallow2 ? shallowReadonlyMap : readonlyMap : isShallow2 ? shallowReactiveMap : reactiveMap).get(target) || // receiver is not the reactive proxy, but has the same prototype + // this means the receiver is a user proxy of the reactive proxy + Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)) { + return target; + } + return; + } + const targetIsArray = isArray(target); + if (!isReadonly2) { + let fn; + if (targetIsArray && (fn = arrayInstrumentations[key])) { + return fn; + } + if (key === "hasOwnProperty") { + return hasOwnProperty2; + } + } + const res = Reflect.get( + target, + key, + // if this is a proxy wrapping a ref, return methods using the raw ref + // as receiver so that we don't have to call `toRaw` on the ref in all + // its class methods + isRef2(target) ? target : receiver + ); + if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { + return res; + } + if (!isReadonly2) { + track(target, "get", key); + } + if (isShallow2) { + return res; + } + if (isRef2(res)) { + const value = targetIsArray && isIntegerKey(key) ? res : res.value; + return isReadonly2 && isObject(value) ? readonly(value) : value; + } + if (isObject(res)) { + return isReadonly2 ? readonly(res) : reactive(res); + } + return res; + } +}; +var MutableReactiveHandler = class extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(false, isShallow2); + } + set(target, key, value, receiver) { + let oldValue = target[key]; + const isArrayWithIntegerKey = isArray(target) && isIntegerKey(key); + if (!this._isShallow) { + const isOldValueReadonly = isReadonly(oldValue); + if (!isShallow(value) && !isReadonly(value)) { + oldValue = toRaw(oldValue); + value = toRaw(value); + } + if (!isArrayWithIntegerKey && isRef2(oldValue) && !isRef2(value)) { + if (isOldValueReadonly) { + if (true) { + warn( + `Set operation on key "${String(key)}" failed: target is readonly.`, + target[key] + ); + } + return true; + } else { + oldValue.value = value; + return true; + } + } + } + const hadKey = isArrayWithIntegerKey ? Number(key) < target.length : hasOwn(target, key); + const result = Reflect.set( + target, + key, + value, + isRef2(target) ? target : receiver + ); + if (target === toRaw(receiver)) { + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + } + return result; + } + deleteProperty(target, key) { + const hadKey = hasOwn(target, key); + const oldValue = target[key]; + const result = Reflect.deleteProperty(target, key); + if (result && hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + } + has(target, key) { + const result = Reflect.has(target, key); + if (!isSymbol(key) || !builtInSymbols.has(key)) { + track(target, "has", key); + } + return result; + } + ownKeys(target) { + track( + target, + "iterate", + isArray(target) ? "length" : ITERATE_KEY + ); + return Reflect.ownKeys(target); + } +}; +var ReadonlyReactiveHandler = class extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(true, isShallow2); + } + set(target, key) { + if (true) { + warn( + `Set operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } + deleteProperty(target, key) { + if (true) { + warn( + `Delete operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } +}; +var mutableHandlers = new MutableReactiveHandler(); +var readonlyHandlers = new ReadonlyReactiveHandler(); +var shallowReactiveHandlers = new MutableReactiveHandler(true); +var shallowReadonlyHandlers = new ReadonlyReactiveHandler(true); +var toShallow = (value) => value; +var getProto = (v) => Reflect.getPrototypeOf(v); +function createIterableMethod(method, isReadonly2, isShallow2) { + return function(...args) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const targetIsMap = isMap(rawTarget); + const isPair = method === "entries" || method === Symbol.iterator && targetIsMap; + const isKeyOnly = method === "keys" && targetIsMap; + const innerIterator = target[method](...args); + const wrap = isShallow2 ? toShallow : isReadonly2 ? toReadonly : toReactive; + !isReadonly2 && track( + rawTarget, + "iterate", + isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY + ); + return extend( + // inheriting all iterator properties + Object.create(innerIterator), + { + // iterator protocol + next() { + const { value, done } = innerIterator.next(); + return done ? { value, done } : { + value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), + done + }; + } + } + ); + }; +} +function createReadonlyMethod(type) { + return function(...args) { + if (true) { + const key = args[0] ? `on key "${args[0]}" ` : ``; + warn( + `${capitalize(type)} operation ${key}failed: target is readonly.`, + toRaw(this) + ); + } + return type === "delete" ? false : type === "clear" ? void 0 : this; + }; +} +function createInstrumentations(readonly2, shallow) { + const instrumentations = { + get(key) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!readonly2) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "get", key); + } + track(rawTarget, "get", rawKey); + } + const { has } = getProto(rawTarget); + const wrap = shallow ? toShallow : readonly2 ? toReadonly : toReactive; + if (has.call(rawTarget, key)) { + return wrap(target.get(key)); + } else if (has.call(rawTarget, rawKey)) { + return wrap(target.get(rawKey)); + } else if (target !== rawTarget) { + target.get(key); + } + }, + get size() { + const target = this["__v_raw"]; + !readonly2 && track(toRaw(target), "iterate", ITERATE_KEY); + return target.size; + }, + has(key) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!readonly2) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "has", key); + } + track(rawTarget, "has", rawKey); + } + return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey); + }, + forEach(callback, thisArg) { + const observed = this; + const target = observed["__v_raw"]; + const rawTarget = toRaw(target); + const wrap = shallow ? toShallow : readonly2 ? toReadonly : toReactive; + !readonly2 && track(rawTarget, "iterate", ITERATE_KEY); + return target.forEach((value, key) => { + return callback.call(thisArg, wrap(value), wrap(key), observed); + }); + } + }; + extend( + instrumentations, + readonly2 ? { + add: createReadonlyMethod("add"), + set: createReadonlyMethod("set"), + delete: createReadonlyMethod("delete"), + clear: createReadonlyMethod("clear") + } : { + add(value) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value); + } + const target = toRaw(this); + const proto = getProto(target); + const hadKey = proto.has.call(target, value); + if (!hadKey) { + target.add(value); + trigger(target, "add", value, value); + } + return this; + }, + set(key, value) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value); + } + const target = toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has.call(target, key); + } else if (true) { + checkIdentityKeys(target, has, key); + } + const oldValue = get.call(target, key); + target.set(key, value); + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + return this; + }, + delete(key) { + const target = toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has.call(target, key); + } else if (true) { + checkIdentityKeys(target, has, key); + } + const oldValue = get ? get.call(target, key) : void 0; + const result = target.delete(key); + if (hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + }, + clear() { + const target = toRaw(this); + const hadItems = target.size !== 0; + const oldTarget = true ? isMap(target) ? new Map(target) : new Set(target) : void 0; + const result = target.clear(); + if (hadItems) { + trigger( + target, + "clear", + void 0, + void 0, + oldTarget + ); + } + return result; + } + } + ); + const iteratorMethods = [ + "keys", + "values", + "entries", + Symbol.iterator + ]; + iteratorMethods.forEach((method) => { + instrumentations[method] = createIterableMethod(method, readonly2, shallow); + }); + return instrumentations; +} +function createInstrumentationGetter(isReadonly2, shallow) { + const instrumentations = createInstrumentations(isReadonly2, shallow); + return (target, key, receiver) => { + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_raw") { + return target; + } + return Reflect.get( + hasOwn(instrumentations, key) && key in target ? instrumentations : target, + key, + receiver + ); + }; +} +var mutableCollectionHandlers = { + get: createInstrumentationGetter(false, false) +}; +var shallowCollectionHandlers = { + get: createInstrumentationGetter(false, true) +}; +var readonlyCollectionHandlers = { + get: createInstrumentationGetter(true, false) +}; +var shallowReadonlyCollectionHandlers = { + get: createInstrumentationGetter(true, true) +}; +function checkIdentityKeys(target, has, key) { + const rawKey = toRaw(key); + if (rawKey !== key && has.call(target, rawKey)) { + const type = toRawType(target); + warn( + `Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.` + ); + } +} +var reactiveMap = /* @__PURE__ */ new WeakMap(); +var shallowReactiveMap = /* @__PURE__ */ new WeakMap(); +var readonlyMap = /* @__PURE__ */ new WeakMap(); +var shallowReadonlyMap = /* @__PURE__ */ new WeakMap(); +function targetTypeMap(rawType) { + switch (rawType) { + case "Object": + case "Array": + return 1; + case "Map": + case "Set": + case "WeakMap": + case "WeakSet": + return 2; + default: + return 0; + } +} +function getTargetType(value) { + return value["__v_skip"] || !Object.isExtensible(value) ? 0 : targetTypeMap(toRawType(value)); +} +function reactive(target) { + if (isReadonly(target)) { + return target; + } + return createReactiveObject( + target, + false, + mutableHandlers, + mutableCollectionHandlers, + reactiveMap + ); +} +function shallowReactive(target) { + return createReactiveObject( + target, + false, + shallowReactiveHandlers, + shallowCollectionHandlers, + shallowReactiveMap + ); +} +function readonly(target) { + return createReactiveObject( + target, + true, + readonlyHandlers, + readonlyCollectionHandlers, + readonlyMap + ); +} +function shallowReadonly(target) { + return createReactiveObject( + target, + true, + shallowReadonlyHandlers, + shallowReadonlyCollectionHandlers, + shallowReadonlyMap + ); +} +function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) { + if (!isObject(target)) { + if (true) { + warn( + `value cannot be made ${isReadonly2 ? "readonly" : "reactive"}: ${String( + target + )}` + ); + } + return target; + } + if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) { + return target; + } + const targetType = getTargetType(target); + if (targetType === 0) { + return target; + } + const existingProxy = proxyMap.get(target); + if (existingProxy) { + return existingProxy; + } + const proxy = new Proxy( + target, + targetType === 2 ? collectionHandlers : baseHandlers + ); + proxyMap.set(target, proxy); + return proxy; +} +function isReactive(value) { + if (isReadonly(value)) { + return isReactive(value["__v_raw"]); + } + return !!(value && value["__v_isReactive"]); +} +function isReadonly(value) { + return !!(value && value["__v_isReadonly"]); +} +function isShallow(value) { + return !!(value && value["__v_isShallow"]); +} +function isProxy(value) { + return value ? !!value["__v_raw"] : false; +} +function toRaw(observed) { + const raw = observed && observed["__v_raw"]; + return raw ? toRaw(raw) : observed; +} +function markRaw(value) { + if (!hasOwn(value, "__v_skip") && Object.isExtensible(value)) { + def(value, "__v_skip", true); + } + return value; +} +var toReactive = (value) => isObject(value) ? reactive(value) : value; +var toReadonly = (value) => isObject(value) ? readonly(value) : value; +function isRef2(r) { + return r ? r["__v_isRef"] === true : false; +} +function ref(value) { + return createRef(value, false); +} +function shallowRef(value) { + return createRef(value, true); +} +function createRef(rawValue, shallow) { + if (isRef2(rawValue)) { + return rawValue; + } + return new RefImpl(rawValue, shallow); +} +var RefImpl = class { + constructor(value, isShallow2) { + this.dep = new Dep(); + this["__v_isRef"] = true; + this["__v_isShallow"] = false; + this._rawValue = isShallow2 ? value : toRaw(value); + this._value = isShallow2 ? value : toReactive(value); + this["__v_isShallow"] = isShallow2; + } + get value() { + if (true) { + this.dep.track({ + target: this, + type: "get", + key: "value" + }); + } else { + this.dep.track(); + } + return this._value; + } + set value(newValue) { + const oldValue = this._rawValue; + const useDirectValue = this["__v_isShallow"] || isShallow(newValue) || isReadonly(newValue); + newValue = useDirectValue ? newValue : toRaw(newValue); + if (hasChanged(newValue, oldValue)) { + this._rawValue = newValue; + this._value = useDirectValue ? newValue : toReactive(newValue); + if (true) { + this.dep.trigger({ + target: this, + type: "set", + key: "value", + newValue, + oldValue + }); + } else { + this.dep.trigger(); + } + } + } +}; +function triggerRef(ref2) { + if (ref2.dep) { + if (true) { + ref2.dep.trigger({ + target: ref2, + type: "set", + key: "value", + newValue: ref2._value + }); + } else { + ref2.dep.trigger(); + } + } +} +function unref(ref2) { + return isRef2(ref2) ? ref2.value : ref2; +} +function toValue(source) { + return isFunction(source) ? source() : unref(source); +} +var shallowUnwrapHandlers = { + get: (target, key, receiver) => key === "__v_raw" ? target : unref(Reflect.get(target, key, receiver)), + set: (target, key, value, receiver) => { + const oldValue = target[key]; + if (isRef2(oldValue) && !isRef2(value)) { + oldValue.value = value; + return true; + } else { + return Reflect.set(target, key, value, receiver); + } + } +}; +function proxyRefs(objectWithRefs) { + return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers); +} +var CustomRefImpl = class { + constructor(factory) { + this["__v_isRef"] = true; + this._value = void 0; + const dep = this.dep = new Dep(); + const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep)); + this._get = get; + this._set = set; + } + get value() { + return this._value = this._get(); + } + set value(newVal) { + this._set(newVal); + } +}; +function customRef(factory) { + return new CustomRefImpl(factory); +} +function toRefs(object) { + if (!isProxy(object)) { + warn(`toRefs() expects a reactive object but received a plain one.`); + } + const ret = isArray(object) ? new Array(object.length) : {}; + for (const key in object) { + ret[key] = propertyToRef(object, key); + } + return ret; +} +var ObjectRefImpl = class { + constructor(_object, _key, _defaultValue) { + this._object = _object; + this._key = _key; + this._defaultValue = _defaultValue; + this["__v_isRef"] = true; + this._value = void 0; + this._raw = toRaw(_object); + let shallow = true; + let obj = _object; + if (!isArray(_object) || !isIntegerKey(String(_key))) { + do { + shallow = !isProxy(obj) || isShallow(obj); + } while (shallow && (obj = obj["__v_raw"])); + } + this._shallow = shallow; + } + get value() { + let val = this._object[this._key]; + if (this._shallow) { + val = unref(val); + } + return this._value = val === void 0 ? this._defaultValue : val; + } + set value(newVal) { + if (this._shallow && isRef2(this._raw[this._key])) { + const nestedRef = this._object[this._key]; + if (isRef2(nestedRef)) { + nestedRef.value = newVal; + return; + } + } + this._object[this._key] = newVal; + } + get dep() { + return getDepFromReactive(this._raw, this._key); + } +}; +var GetterRefImpl = class { + constructor(_getter) { + this._getter = _getter; + this["__v_isRef"] = true; + this["__v_isReadonly"] = true; + this._value = void 0; + } + get value() { + return this._value = this._getter(); + } +}; +function toRef(source, key, defaultValue) { + if (isRef2(source)) { + return source; + } else if (isFunction(source)) { + return new GetterRefImpl(source); + } else if (isObject(source) && arguments.length > 1) { + return propertyToRef(source, key, defaultValue); + } else { + return ref(source); + } +} +function propertyToRef(source, key, defaultValue) { + return new ObjectRefImpl(source, key, defaultValue); +} +var ComputedRefImpl = class { + constructor(fn, setter, isSSR) { + this.fn = fn; + this.setter = setter; + this._value = void 0; + this.dep = new Dep(this); + this.__v_isRef = true; + this.deps = void 0; + this.depsTail = void 0; + this.flags = 16; + this.globalVersion = globalVersion - 1; + this.next = void 0; + this.effect = this; + this["__v_isReadonly"] = !setter; + this.isSSR = isSSR; + } + /** + * @internal + */ + notify() { + this.flags |= 16; + if (!(this.flags & 8) && // avoid infinite self recursion + activeSub !== this) { + batch(this, true); + return true; + } else if (true) ; + } + get value() { + const link = true ? this.dep.track({ + target: this, + type: "get", + key: "value" + }) : this.dep.track(); + refreshComputed(this); + if (link) { + link.version = this.dep.version; + } + return this._value; + } + set value(newValue) { + if (this.setter) { + this.setter(newValue); + } else if (true) { + warn("Write operation failed: computed value is readonly"); + } + } +}; +function computed(getterOrOptions, debugOptions, isSSR = false) { + let getter; + let setter; + if (isFunction(getterOrOptions)) { + getter = getterOrOptions; + } else { + getter = getterOrOptions.get; + setter = getterOrOptions.set; + } + const cRef = new ComputedRefImpl(getter, setter, isSSR); + if (debugOptions && !isSSR) { + cRef.onTrack = debugOptions.onTrack; + cRef.onTrigger = debugOptions.onTrigger; + } + return cRef; +} +var TrackOpTypes = { + "GET": "get", + "HAS": "has", + "ITERATE": "iterate" +}; +var TriggerOpTypes = { + "SET": "set", + "ADD": "add", + "DELETE": "delete", + "CLEAR": "clear" +}; +var INITIAL_WATCHER_VALUE = {}; +var cleanupMap = /* @__PURE__ */ new WeakMap(); +var activeWatcher = void 0; +function getCurrentWatcher() { + return activeWatcher; +} +function onWatcherCleanup(cleanupFn, failSilently = false, owner = activeWatcher) { + if (owner) { + let cleanups = cleanupMap.get(owner); + if (!cleanups) cleanupMap.set(owner, cleanups = []); + cleanups.push(cleanupFn); + } else if (!failSilently) { + warn( + `onWatcherCleanup() was called when there was no active watcher to associate with.` + ); + } +} +function watch(source, cb, options = EMPTY_OBJ) { + const { immediate, deep, once, scheduler, augmentJob, call } = options; + const warnInvalidSource = (s) => { + (options.onWarn || warn)( + `Invalid watch source: `, + s, + `A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.` + ); + }; + const reactiveGetter = (source2) => { + if (deep) return source2; + if (isShallow(source2) || deep === false || deep === 0) + return traverse(source2, 1); + return traverse(source2); + }; + let effect2; + let getter; + let cleanup; + let boundCleanup; + let forceTrigger = false; + let isMultiSource = false; + if (isRef2(source)) { + getter = () => source.value; + forceTrigger = isShallow(source); + } else if (isReactive(source)) { + getter = () => reactiveGetter(source); + forceTrigger = true; + } else if (isArray(source)) { + isMultiSource = true; + forceTrigger = source.some((s) => isReactive(s) || isShallow(s)); + getter = () => source.map((s) => { + if (isRef2(s)) { + return s.value; + } else if (isReactive(s)) { + return reactiveGetter(s); + } else if (isFunction(s)) { + return call ? call(s, 2) : s(); + } else { + warnInvalidSource(s); + } + }); + } else if (isFunction(source)) { + if (cb) { + getter = call ? () => call(source, 2) : source; + } else { + getter = () => { + if (cleanup) { + pauseTracking(); + try { + cleanup(); + } finally { + resetTracking(); + } + } + const currentEffect = activeWatcher; + activeWatcher = effect2; + try { + return call ? call(source, 3, [boundCleanup]) : source(boundCleanup); + } finally { + activeWatcher = currentEffect; + } + }; + } + } else { + getter = NOOP; + warnInvalidSource(source); + } + if (cb && deep) { + const baseGetter = getter; + const depth = deep === true ? Infinity : deep; + getter = () => traverse(baseGetter(), depth); + } + const scope = getCurrentScope(); + const watchHandle = () => { + effect2.stop(); + if (scope && scope.active) { + remove(scope.effects, effect2); + } + }; + if (once && cb) { + const _cb = cb; + cb = (...args) => { + _cb(...args); + watchHandle(); + }; + } + let oldValue = isMultiSource ? new Array(source.length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE; + const job = (immediateFirstRun) => { + if (!(effect2.flags & 1) || !effect2.dirty && !immediateFirstRun) { + return; + } + if (cb) { + const newValue = effect2.run(); + if (deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => hasChanged(v, oldValue[i])) : hasChanged(newValue, oldValue))) { + if (cleanup) { + cleanup(); + } + const currentWatcher = activeWatcher; + activeWatcher = effect2; + try { + const args = [ + newValue, + // pass undefined as the old value when it's changed for the first time + oldValue === INITIAL_WATCHER_VALUE ? void 0 : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, + boundCleanup + ]; + oldValue = newValue; + call ? call(cb, 3, args) : ( + // @ts-expect-error + cb(...args) + ); + } finally { + activeWatcher = currentWatcher; + } + } + } else { + effect2.run(); + } + }; + if (augmentJob) { + augmentJob(job); + } + effect2 = new ReactiveEffect(getter); + effect2.scheduler = scheduler ? () => scheduler(job, false) : job; + boundCleanup = (fn) => onWatcherCleanup(fn, false, effect2); + cleanup = effect2.onStop = () => { + const cleanups = cleanupMap.get(effect2); + if (cleanups) { + if (call) { + call(cleanups, 4); + } else { + for (const cleanup2 of cleanups) cleanup2(); + } + cleanupMap.delete(effect2); + } + }; + if (true) { + effect2.onTrack = options.onTrack; + effect2.onTrigger = options.onTrigger; + } + if (cb) { + if (immediate) { + job(true); + } else { + oldValue = effect2.run(); + } + } else if (scheduler) { + scheduler(job.bind(null, true), true); + } else { + effect2.run(); + } + watchHandle.pause = effect2.pause.bind(effect2); + watchHandle.resume = effect2.resume.bind(effect2); + watchHandle.stop = watchHandle; + return watchHandle; +} +function traverse(value, depth = Infinity, seen) { + if (depth <= 0 || !isObject(value) || value["__v_skip"]) { + return value; + } + seen = seen || /* @__PURE__ */ new Map(); + if ((seen.get(value) || 0) >= depth) { + return value; + } + seen.set(value, depth); + depth--; + if (isRef2(value)) { + traverse(value.value, depth, seen); + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], depth, seen); + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v) => { + traverse(v, depth, seen); + }); + } else if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key], depth, seen); + } + for (const key of Object.getOwnPropertySymbols(value)) { + if (Object.prototype.propertyIsEnumerable.call(value, key)) { + traverse(value[key], depth, seen); + } + } + } + return value; +} + +// node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js +var stack = []; +function pushWarningContext(vnode) { + stack.push(vnode); +} +function popWarningContext() { + stack.pop(); +} +var isWarning = false; +function warn$1(msg, ...args) { + if (isWarning) return; + isWarning = true; + pauseTracking(); + const instance = stack.length ? stack[stack.length - 1].component : null; + const appWarnHandler = instance && instance.appContext.config.warnHandler; + const trace = getComponentTrace(); + if (appWarnHandler) { + callWithErrorHandling( + appWarnHandler, + instance, + 11, + [ + // eslint-disable-next-line no-restricted-syntax + msg + args.map((a) => { + var _a, _b; + return (_b = (_a = a.toString) == null ? void 0 : _a.call(a)) != null ? _b : JSON.stringify(a); + }).join(""), + instance && instance.proxy, + trace.map( + ({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>` + ).join("\n"), + trace + ] + ); + } else { + const warnArgs = [`[Vue warn]: ${msg}`, ...args]; + if (trace.length && // avoid spamming console during tests + true) { + warnArgs.push(` +`, ...formatTrace(trace)); + } + console.warn(...warnArgs); + } + resetTracking(); + isWarning = false; +} +function getComponentTrace() { + let currentVNode = stack[stack.length - 1]; + if (!currentVNode) { + return []; + } + const normalizedStack = []; + while (currentVNode) { + const last = normalizedStack[0]; + if (last && last.vnode === currentVNode) { + last.recurseCount++; + } else { + normalizedStack.push({ + vnode: currentVNode, + recurseCount: 0 + }); + } + const parentInstance = currentVNode.component && currentVNode.component.parent; + currentVNode = parentInstance && parentInstance.vnode; + } + return normalizedStack; +} +function formatTrace(trace) { + const logs = []; + trace.forEach((entry, i) => { + logs.push(...i === 0 ? [] : [` +`], ...formatTraceEntry(entry)); + }); + return logs; +} +function formatTraceEntry({ vnode, recurseCount }) { + const postfix = recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``; + const isRoot = vnode.component ? vnode.component.parent == null : false; + const open = ` at <${formatComponentName( + vnode.component, + vnode.type, + isRoot + )}`; + const close = `>` + postfix; + return vnode.props ? [open, ...formatProps(vnode.props), close] : [open + close]; +} +function formatProps(props) { + const res = []; + const keys = Object.keys(props); + keys.slice(0, 3).forEach((key) => { + res.push(...formatProp(key, props[key])); + }); + if (keys.length > 3) { + res.push(` ...`); + } + return res; +} +function formatProp(key, value, raw) { + if (isString(value)) { + value = JSON.stringify(value); + return raw ? value : [`${key}=${value}`]; + } else if (typeof value === "number" || typeof value === "boolean" || value == null) { + return raw ? value : [`${key}=${value}`]; + } else if (isRef2(value)) { + value = formatProp(key, toRaw(value.value), true); + return raw ? value : [`${key}=Ref<`, value, `>`]; + } else if (isFunction(value)) { + return [`${key}=fn${value.name ? `<${value.name}>` : ``}`]; + } else { + value = toRaw(value); + return raw ? value : [`${key}=`, value]; + } +} +function assertNumber(val, type) { + if (false) return; + if (val === void 0) { + return; + } else if (typeof val !== "number") { + warn$1(`${type} is not a valid number - got ${JSON.stringify(val)}.`); + } else if (isNaN(val)) { + warn$1(`${type} is NaN - the duration expression might be incorrect.`); + } +} +var ErrorCodes = { + "SETUP_FUNCTION": 0, + "0": "SETUP_FUNCTION", + "RENDER_FUNCTION": 1, + "1": "RENDER_FUNCTION", + "NATIVE_EVENT_HANDLER": 5, + "5": "NATIVE_EVENT_HANDLER", + "COMPONENT_EVENT_HANDLER": 6, + "6": "COMPONENT_EVENT_HANDLER", + "VNODE_HOOK": 7, + "7": "VNODE_HOOK", + "DIRECTIVE_HOOK": 8, + "8": "DIRECTIVE_HOOK", + "TRANSITION_HOOK": 9, + "9": "TRANSITION_HOOK", + "APP_ERROR_HANDLER": 10, + "10": "APP_ERROR_HANDLER", + "APP_WARN_HANDLER": 11, + "11": "APP_WARN_HANDLER", + "FUNCTION_REF": 12, + "12": "FUNCTION_REF", + "ASYNC_COMPONENT_LOADER": 13, + "13": "ASYNC_COMPONENT_LOADER", + "SCHEDULER": 14, + "14": "SCHEDULER", + "COMPONENT_UPDATE": 15, + "15": "COMPONENT_UPDATE", + "APP_UNMOUNT_CLEANUP": 16, + "16": "APP_UNMOUNT_CLEANUP" +}; +var ErrorTypeStrings$1 = { + ["sp"]: "serverPrefetch hook", + ["bc"]: "beforeCreate hook", + ["c"]: "created hook", + ["bm"]: "beforeMount hook", + ["m"]: "mounted hook", + ["bu"]: "beforeUpdate hook", + ["u"]: "updated", + ["bum"]: "beforeUnmount hook", + ["um"]: "unmounted hook", + ["a"]: "activated hook", + ["da"]: "deactivated hook", + ["ec"]: "errorCaptured hook", + ["rtc"]: "renderTracked hook", + ["rtg"]: "renderTriggered hook", + [0]: "setup function", + [1]: "render function", + [2]: "watcher getter", + [3]: "watcher callback", + [4]: "watcher cleanup function", + [5]: "native event handler", + [6]: "component event handler", + [7]: "vnode hook", + [8]: "directive hook", + [9]: "transition hook", + [10]: "app errorHandler", + [11]: "app warnHandler", + [12]: "ref function", + [13]: "async component loader", + [14]: "scheduler flush", + [15]: "component update", + [16]: "app unmount cleanup function" +}; +function callWithErrorHandling(fn, instance, type, args) { + try { + return args ? fn(...args) : fn(); + } catch (err) { + handleError(err, instance, type); + } +} +function callWithAsyncErrorHandling(fn, instance, type, args) { + if (isFunction(fn)) { + const res = callWithErrorHandling(fn, instance, type, args); + if (res && isPromise(res)) { + res.catch((err) => { + handleError(err, instance, type); + }); + } + return res; + } + if (isArray(fn)) { + const values = []; + for (let i = 0; i < fn.length; i++) { + values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)); + } + return values; + } else if (true) { + warn$1( + `Invalid value type passed to callWithAsyncErrorHandling(): ${typeof fn}` + ); + } +} +function handleError(err, instance, type, throwInDev = true) { + const contextVNode = instance ? instance.vnode : null; + const { errorHandler, throwUnhandledErrorInProduction } = instance && instance.appContext.config || EMPTY_OBJ; + if (instance) { + let cur = instance.parent; + const exposedInstance = instance.proxy; + const errorInfo = true ? ErrorTypeStrings$1[type] : `https://vuejs.org/error-reference/#runtime-${type}`; + while (cur) { + const errorCapturedHooks = cur.ec; + if (errorCapturedHooks) { + for (let i = 0; i < errorCapturedHooks.length; i++) { + if (errorCapturedHooks[i](err, exposedInstance, errorInfo) === false) { + return; + } + } + } + cur = cur.parent; + } + if (errorHandler) { + pauseTracking(); + callWithErrorHandling(errorHandler, null, 10, [ + err, + exposedInstance, + errorInfo + ]); + resetTracking(); + return; + } + } + logError(err, type, contextVNode, throwInDev, throwUnhandledErrorInProduction); +} +function logError(err, type, contextVNode, throwInDev = true, throwInProd = false) { + if (true) { + const info = ErrorTypeStrings$1[type]; + if (contextVNode) { + pushWarningContext(contextVNode); + } + warn$1(`Unhandled error${info ? ` during execution of ${info}` : ``}`); + if (contextVNode) { + popWarningContext(); + } + if (throwInDev) { + throw err; + } else { + console.error(err); + } + } else if (throwInProd) { + throw err; + } else { + console.error(err); + } +} +var queue = []; +var flushIndex = -1; +var pendingPostFlushCbs = []; +var activePostFlushCbs = null; +var postFlushIndex = 0; +var resolvedPromise = Promise.resolve(); +var currentFlushPromise = null; +var RECURSION_LIMIT = 100; +function nextTick(fn) { + const p2 = currentFlushPromise || resolvedPromise; + return fn ? p2.then(this ? fn.bind(this) : fn) : p2; +} +function findInsertionIndex(id) { + let start = flushIndex + 1; + let end = queue.length; + while (start < end) { + const middle = start + end >>> 1; + const middleJob = queue[middle]; + const middleJobId = getId(middleJob); + if (middleJobId < id || middleJobId === id && middleJob.flags & 2) { + start = middle + 1; + } else { + end = middle; + } + } + return start; +} +function queueJob(job) { + if (!(job.flags & 1)) { + const jobId = getId(job); + const lastJob = queue[queue.length - 1]; + if (!lastJob || // fast path when the job id is larger than the tail + !(job.flags & 2) && jobId >= getId(lastJob)) { + queue.push(job); + } else { + queue.splice(findInsertionIndex(jobId), 0, job); + } + job.flags |= 1; + queueFlush(); + } +} +function queueFlush() { + if (!currentFlushPromise) { + currentFlushPromise = resolvedPromise.then(flushJobs); + } +} +function queuePostFlushCb(cb) { + if (!isArray(cb)) { + if (activePostFlushCbs && cb.id === -1) { + activePostFlushCbs.splice(postFlushIndex + 1, 0, cb); + } else if (!(cb.flags & 1)) { + pendingPostFlushCbs.push(cb); + cb.flags |= 1; + } + } else { + pendingPostFlushCbs.push(...cb); + } + queueFlush(); +} +function flushPreFlushCbs(instance, seen, i = flushIndex + 1) { + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + for (; i < queue.length; i++) { + const cb = queue[i]; + if (cb && cb.flags & 2) { + if (instance && cb.id !== instance.uid) { + continue; + } + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + queue.splice(i, 1); + i--; + if (cb.flags & 4) { + cb.flags &= -2; + } + cb(); + if (!(cb.flags & 4)) { + cb.flags &= -2; + } + } + } +} +function flushPostFlushCbs(seen) { + if (pendingPostFlushCbs.length) { + const deduped = [...new Set(pendingPostFlushCbs)].sort( + (a, b) => getId(a) - getId(b) + ); + pendingPostFlushCbs.length = 0; + if (activePostFlushCbs) { + activePostFlushCbs.push(...deduped); + return; + } + activePostFlushCbs = deduped; + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) { + const cb = activePostFlushCbs[postFlushIndex]; + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + if (cb.flags & 4) { + cb.flags &= -2; + } + if (!(cb.flags & 8)) cb(); + cb.flags &= -2; + } + activePostFlushCbs = null; + postFlushIndex = 0; + } +} +var getId = (job) => job.id == null ? job.flags & 2 ? -1 : Infinity : job.id; +function flushJobs(seen) { + if (true) { + seen = seen || /* @__PURE__ */ new Map(); + } + const check = true ? (job) => checkRecursiveUpdates(seen, job) : NOOP; + try { + for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job && !(job.flags & 8)) { + if (check(job)) { + continue; + } + if (job.flags & 4) { + job.flags &= ~1; + } + callWithErrorHandling( + job, + job.i, + job.i ? 15 : 14 + ); + if (!(job.flags & 4)) { + job.flags &= ~1; + } + } + } + } finally { + for (; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job) { + job.flags &= -2; + } + } + flushIndex = -1; + queue.length = 0; + flushPostFlushCbs(seen); + currentFlushPromise = null; + if (queue.length || pendingPostFlushCbs.length) { + flushJobs(seen); + } + } +} +function checkRecursiveUpdates(seen, fn) { + const count = seen.get(fn) || 0; + if (count > RECURSION_LIMIT) { + const instance = fn.i; + const componentName = instance && getComponentName(instance.type); + handleError( + `Maximum recursive updates exceeded${componentName ? ` in component <${componentName}>` : ``}. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.`, + null, + 10 + ); + return true; + } + seen.set(fn, count + 1); + return false; +} +var isHmrUpdating = false; +var hmrDirtyComponents = /* @__PURE__ */ new Map(); +if (true) { + getGlobalThis().__VUE_HMR_RUNTIME__ = { + createRecord: tryWrap(createRecord), + rerender: tryWrap(rerender), + reload: tryWrap(reload) + }; +} +var map = /* @__PURE__ */ new Map(); +function registerHMR(instance) { + const id = instance.type.__hmrId; + let record = map.get(id); + if (!record) { + createRecord(id, instance.type); + record = map.get(id); + } + record.instances.add(instance); +} +function unregisterHMR(instance) { + map.get(instance.type.__hmrId).instances.delete(instance); +} +function createRecord(id, initialDef) { + if (map.has(id)) { + return false; + } + map.set(id, { + initialDef: normalizeClassComponent(initialDef), + instances: /* @__PURE__ */ new Set() + }); + return true; +} +function normalizeClassComponent(component) { + return isClassComponent(component) ? component.__vccOpts : component; +} +function rerender(id, newRender) { + const record = map.get(id); + if (!record) { + return; + } + record.initialDef.render = newRender; + [...record.instances].forEach((instance) => { + if (newRender) { + instance.render = newRender; + normalizeClassComponent(instance.type).render = newRender; + } + instance.renderCache = []; + isHmrUpdating = true; + if (!(instance.job.flags & 8)) { + instance.update(); + } + isHmrUpdating = false; + }); +} +function reload(id, newComp) { + const record = map.get(id); + if (!record) return; + newComp = normalizeClassComponent(newComp); + updateComponentDef(record.initialDef, newComp); + const instances = [...record.instances]; + for (let i = 0; i < instances.length; i++) { + const instance = instances[i]; + const oldComp = normalizeClassComponent(instance.type); + let dirtyInstances = hmrDirtyComponents.get(oldComp); + if (!dirtyInstances) { + if (oldComp !== record.initialDef) { + updateComponentDef(oldComp, newComp); + } + hmrDirtyComponents.set(oldComp, dirtyInstances = /* @__PURE__ */ new Set()); + } + dirtyInstances.add(instance); + instance.appContext.propsCache.delete(instance.type); + instance.appContext.emitsCache.delete(instance.type); + instance.appContext.optionsCache.delete(instance.type); + if (instance.ceReload) { + dirtyInstances.add(instance); + instance.ceReload(newComp.styles); + dirtyInstances.delete(instance); + } else if (instance.parent) { + queueJob(() => { + if (!(instance.job.flags & 8)) { + isHmrUpdating = true; + instance.parent.update(); + isHmrUpdating = false; + dirtyInstances.delete(instance); + } + }); + } else if (instance.appContext.reload) { + instance.appContext.reload(); + } else if (typeof window !== "undefined") { + window.location.reload(); + } else { + console.warn( + "[HMR] Root or manually mounted instance modified. Full reload required." + ); + } + if (instance.root.ce && instance !== instance.root) { + instance.root.ce._removeChildStyle(oldComp); + } + } + queuePostFlushCb(() => { + hmrDirtyComponents.clear(); + }); +} +function updateComponentDef(oldComp, newComp) { + extend(oldComp, newComp); + for (const key in oldComp) { + if (key !== "__file" && !(key in newComp)) { + delete oldComp[key]; + } + } +} +function tryWrap(fn) { + return (id, arg) => { + try { + return fn(id, arg); + } catch (e) { + console.error(e); + console.warn( + `[HMR] Something went wrong during Vue component hot-reload. Full reload required.` + ); + } + }; +} +var devtools$1; +var buffer = []; +var devtoolsNotInstalled = false; +function emit$1(event, ...args) { + if (devtools$1) { + devtools$1.emit(event, ...args); + } else if (!devtoolsNotInstalled) { + buffer.push({ event, args }); + } +} +function setDevtoolsHook$1(hook, target) { + var _a, _b; + devtools$1 = hook; + if (devtools$1) { + devtools$1.enabled = true; + buffer.forEach(({ event, args }) => devtools$1.emit(event, ...args)); + buffer = []; + } else if ( + // handle late devtools injection - only do this if we are in an actual + // browser environment to avoid the timer handle stalling test runner exit + // (#4815) + typeof window !== "undefined" && // some envs mock window but not fully + window.HTMLElement && // also exclude jsdom + // eslint-disable-next-line no-restricted-syntax + !((_b = (_a = window.navigator) == null ? void 0 : _a.userAgent) == null ? void 0 : _b.includes("jsdom")) + ) { + const replay = target.__VUE_DEVTOOLS_HOOK_REPLAY__ = target.__VUE_DEVTOOLS_HOOK_REPLAY__ || []; + replay.push((newHook) => { + setDevtoolsHook$1(newHook, target); + }); + setTimeout(() => { + if (!devtools$1) { + target.__VUE_DEVTOOLS_HOOK_REPLAY__ = null; + devtoolsNotInstalled = true; + buffer = []; + } + }, 3e3); + } else { + devtoolsNotInstalled = true; + buffer = []; + } +} +function devtoolsInitApp(app, version2) { + emit$1("app:init", app, version2, { + Fragment, + Text, + Comment, + Static + }); +} +function devtoolsUnmountApp(app) { + emit$1("app:unmount", app); +} +var devtoolsComponentAdded = createDevtoolsComponentHook( + "component:added" + /* COMPONENT_ADDED */ +); +var devtoolsComponentUpdated = createDevtoolsComponentHook( + "component:updated" + /* COMPONENT_UPDATED */ +); +var _devtoolsComponentRemoved = createDevtoolsComponentHook( + "component:removed" + /* COMPONENT_REMOVED */ +); +var devtoolsComponentRemoved = (component) => { + if (devtools$1 && typeof devtools$1.cleanupBuffer === "function" && // remove the component if it wasn't buffered + !devtools$1.cleanupBuffer(component)) { + _devtoolsComponentRemoved(component); + } +}; +function createDevtoolsComponentHook(hook) { + return (component) => { + emit$1( + hook, + component.appContext.app, + component.uid, + component.parent ? component.parent.uid : void 0, + component + ); + }; +} +var devtoolsPerfStart = createDevtoolsPerformanceHook( + "perf:start" + /* PERFORMANCE_START */ +); +var devtoolsPerfEnd = createDevtoolsPerformanceHook( + "perf:end" + /* PERFORMANCE_END */ +); +function createDevtoolsPerformanceHook(hook) { + return (component, type, time) => { + emit$1(hook, component.appContext.app, component.uid, component, type, time); + }; +} +function devtoolsComponentEmit(component, event, params) { + emit$1( + "component:emit", + component.appContext.app, + component, + event, + params + ); +} +var currentRenderingInstance = null; +var currentScopeId = null; +function setCurrentRenderingInstance(instance) { + const prev = currentRenderingInstance; + currentRenderingInstance = instance; + currentScopeId = instance && instance.type.__scopeId || null; + return prev; +} +function pushScopeId(id) { + currentScopeId = id; +} +function popScopeId() { + currentScopeId = null; +} +var withScopeId = (_id) => withCtx; +function withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot) { + if (!ctx) return fn; + if (fn._n) { + return fn; + } + const renderFnWithContext = (...args) => { + if (renderFnWithContext._d) { + setBlockTracking(-1); + } + const prevInstance = setCurrentRenderingInstance(ctx); + let res; + try { + res = fn(...args); + } finally { + setCurrentRenderingInstance(prevInstance); + if (renderFnWithContext._d) { + setBlockTracking(1); + } + } + if (true) { + devtoolsComponentUpdated(ctx); + } + return res; + }; + renderFnWithContext._n = true; + renderFnWithContext._c = true; + renderFnWithContext._d = true; + return renderFnWithContext; +} +function validateDirectiveName(name) { + if (isBuiltInDirective(name)) { + warn$1("Do not use built-in directive ids as custom directive id: " + name); + } +} +function withDirectives(vnode, directives) { + if (currentRenderingInstance === null) { + warn$1(`withDirectives can only be used inside render functions.`); + return vnode; + } + const instance = getComponentPublicInstance(currentRenderingInstance); + const bindings = vnode.dirs || (vnode.dirs = []); + for (let i = 0; i < directives.length; i++) { + let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]; + if (dir) { + if (isFunction(dir)) { + dir = { + mounted: dir, + updated: dir + }; + } + if (dir.deep) { + traverse(value); + } + bindings.push({ + dir, + instance, + value, + oldValue: void 0, + arg, + modifiers + }); + } + } + return vnode; +} +function invokeDirectiveHook(vnode, prevVNode, instance, name) { + const bindings = vnode.dirs; + const oldBindings = prevVNode && prevVNode.dirs; + for (let i = 0; i < bindings.length; i++) { + const binding = bindings[i]; + if (oldBindings) { + binding.oldValue = oldBindings[i].value; + } + let hook = binding.dir[name]; + if (hook) { + pauseTracking(); + callWithAsyncErrorHandling(hook, instance, 8, [ + vnode.el, + binding, + vnode, + prevVNode + ]); + resetTracking(); + } + } +} +function provide(key, value) { + if (true) { + if (!currentInstance || currentInstance.isMounted) { + warn$1(`provide() can only be used inside setup().`); + } + } + if (currentInstance) { + let provides = currentInstance.provides; + const parentProvides = currentInstance.parent && currentInstance.parent.provides; + if (parentProvides === provides) { + provides = currentInstance.provides = Object.create(parentProvides); + } + provides[key] = value; + } +} +function inject(key, defaultValue, treatDefaultAsFactory = false) { + const instance = getCurrentInstance(); + if (instance || currentApp) { + let provides = currentApp ? currentApp._context.provides : instance ? instance.parent == null || instance.ce ? instance.vnode.appContext && instance.vnode.appContext.provides : instance.parent.provides : void 0; + if (provides && key in provides) { + return provides[key]; + } else if (arguments.length > 1) { + return treatDefaultAsFactory && isFunction(defaultValue) ? defaultValue.call(instance && instance.proxy) : defaultValue; + } else if (true) { + warn$1(`injection "${String(key)}" not found.`); + } + } else if (true) { + warn$1(`inject() can only be used inside setup() or functional components.`); + } +} +function hasInjectionContext() { + return !!(getCurrentInstance() || currentApp); +} +var ssrContextKey = /* @__PURE__ */ Symbol.for("v-scx"); +var useSSRContext = () => { + { + const ctx = inject(ssrContextKey); + if (!ctx) { + warn$1( + `Server rendering context not provided. Make sure to only call useSSRContext() conditionally in the server build.` + ); + } + return ctx; + } +}; +function watchEffect(effect2, options) { + return doWatch(effect2, null, options); +} +function watchPostEffect(effect2, options) { + return doWatch( + effect2, + null, + true ? extend({}, options, { flush: "post" }) : { flush: "post" } + ); +} +function watchSyncEffect(effect2, options) { + return doWatch( + effect2, + null, + true ? extend({}, options, { flush: "sync" }) : { flush: "sync" } + ); +} +function watch2(source, cb, options) { + if (!isFunction(cb)) { + warn$1( + `\`watch(fn, options?)\` signature has been moved to a separate API. Use \`watchEffect(fn, options?)\` instead. \`watch\` now only supports \`watch(source, cb, options?) signature.` + ); + } + return doWatch(source, cb, options); +} +function doWatch(source, cb, options = EMPTY_OBJ) { + const { immediate, deep, flush, once } = options; + if (!cb) { + if (immediate !== void 0) { + warn$1( + `watch() "immediate" option is only respected when using the watch(source, callback, options?) signature.` + ); + } + if (deep !== void 0) { + warn$1( + `watch() "deep" option is only respected when using the watch(source, callback, options?) signature.` + ); + } + if (once !== void 0) { + warn$1( + `watch() "once" option is only respected when using the watch(source, callback, options?) signature.` + ); + } + } + const baseWatchOptions = extend({}, options); + if (true) baseWatchOptions.onWarn = warn$1; + const runsImmediately = cb && immediate || !cb && flush !== "post"; + let ssrCleanup; + if (isInSSRComponentSetup) { + if (flush === "sync") { + const ctx = useSSRContext(); + ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []); + } else if (!runsImmediately) { + const watchStopHandle = () => { + }; + watchStopHandle.stop = NOOP; + watchStopHandle.resume = NOOP; + watchStopHandle.pause = NOOP; + return watchStopHandle; + } + } + const instance = currentInstance; + baseWatchOptions.call = (fn, type, args) => callWithAsyncErrorHandling(fn, instance, type, args); + let isPre = false; + if (flush === "post") { + baseWatchOptions.scheduler = (job) => { + queuePostRenderEffect(job, instance && instance.suspense); + }; + } else if (flush !== "sync") { + isPre = true; + baseWatchOptions.scheduler = (job, isFirstRun) => { + if (isFirstRun) { + job(); + } else { + queueJob(job); + } + }; + } + baseWatchOptions.augmentJob = (job) => { + if (cb) { + job.flags |= 4; + } + if (isPre) { + job.flags |= 2; + if (instance) { + job.id = instance.uid; + job.i = instance; + } + } + }; + const watchHandle = watch(source, cb, baseWatchOptions); + if (isInSSRComponentSetup) { + if (ssrCleanup) { + ssrCleanup.push(watchHandle); + } else if (runsImmediately) { + watchHandle(); + } + } + return watchHandle; +} +function instanceWatch(source, value, options) { + const publicThis = this.proxy; + const getter = isString(source) ? source.includes(".") ? createPathGetter(publicThis, source) : () => publicThis[source] : source.bind(publicThis, publicThis); + let cb; + if (isFunction(value)) { + cb = value; + } else { + cb = value.handler; + options = value; + } + const reset = setCurrentInstance(this); + const res = doWatch(getter, cb.bind(publicThis), options); + reset(); + return res; +} +function createPathGetter(ctx, path) { + const segments = path.split("."); + return () => { + let cur = ctx; + for (let i = 0; i < segments.length && cur; i++) { + cur = cur[segments[i]]; + } + return cur; + }; +} +var TeleportEndKey = /* @__PURE__ */ Symbol("_vte"); +var isTeleport = (type) => type.__isTeleport; +var isTeleportDisabled = (props) => props && (props.disabled || props.disabled === ""); +var isTeleportDeferred = (props) => props && (props.defer || props.defer === ""); +var isTargetSVG = (target) => typeof SVGElement !== "undefined" && target instanceof SVGElement; +var isTargetMathML = (target) => typeof MathMLElement === "function" && target instanceof MathMLElement; +var resolveTarget = (props, select) => { + const targetSelector = props && props.to; + if (isString(targetSelector)) { + if (!select) { + warn$1( + `Current renderer does not support string target for Teleports. (missing querySelector renderer option)` + ); + return null; + } else { + const target = select(targetSelector); + if (!target && !isTeleportDisabled(props)) { + warn$1( + `Failed to locate Teleport target with selector "${targetSelector}". Note the target element must exist before the component is mounted - i.e. the target cannot be rendered by the component itself, and ideally should be outside of the entire Vue component tree.` + ); + } + return target; + } + } else { + if (!targetSelector && !isTeleportDisabled(props)) { + warn$1(`Invalid Teleport target: ${targetSelector}`); + } + return targetSelector; + } +}; +var TeleportImpl = { + name: "Teleport", + __isTeleport: true, + process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals) { + const { + mc: mountChildren, + pc: patchChildren, + pbc: patchBlockChildren, + o: { insert, querySelector, createText, createComment } + } = internals; + const disabled = isTeleportDisabled(n2.props); + let { shapeFlag, children, dynamicChildren } = n2; + if (isHmrUpdating) { + optimized = false; + dynamicChildren = null; + } + if (n1 == null) { + const placeholder = n2.el = true ? createComment("teleport start") : createText(""); + const mainAnchor = n2.anchor = true ? createComment("teleport end") : createText(""); + insert(placeholder, container, anchor); + insert(mainAnchor, container, anchor); + const mount = (container2, anchor2) => { + if (shapeFlag & 16) { + mountChildren( + children, + container2, + anchor2, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } + }; + const mountToTarget = () => { + const target = n2.target = resolveTarget(n2.props, querySelector); + const targetAnchor = prepareAnchor(target, n2, createText, insert); + if (target) { + if (namespace !== "svg" && isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace !== "mathml" && isTargetMathML(target)) { + namespace = "mathml"; + } + if (parentComponent && parentComponent.isCE) { + (parentComponent.ce._teleportTargets || (parentComponent.ce._teleportTargets = /* @__PURE__ */ new Set())).add(target); + } + if (!disabled) { + mount(target, targetAnchor); + updateCssVars(n2, false); + } + } else if (!disabled) { + warn$1( + "Invalid Teleport target on mount:", + target, + `(${typeof target})` + ); + } + }; + if (disabled) { + mount(container, mainAnchor); + updateCssVars(n2, true); + } + if (isTeleportDeferred(n2.props)) { + n2.el.__isMounted = false; + queuePostRenderEffect(() => { + mountToTarget(); + delete n2.el.__isMounted; + }, parentSuspense); + } else { + mountToTarget(); + } + } else { + if (isTeleportDeferred(n2.props) && n1.el.__isMounted === false) { + queuePostRenderEffect(() => { + TeleportImpl.process( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized, + internals + ); + }, parentSuspense); + return; + } + n2.el = n1.el; + n2.targetStart = n1.targetStart; + const mainAnchor = n2.anchor = n1.anchor; + const target = n2.target = n1.target; + const targetAnchor = n2.targetAnchor = n1.targetAnchor; + const wasDisabled = isTeleportDisabled(n1.props); + const currentContainer = wasDisabled ? container : target; + const currentAnchor = wasDisabled ? mainAnchor : targetAnchor; + if (namespace === "svg" || isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace === "mathml" || isTargetMathML(target)) { + namespace = "mathml"; + } + if (dynamicChildren) { + patchBlockChildren( + n1.dynamicChildren, + dynamicChildren, + currentContainer, + parentComponent, + parentSuspense, + namespace, + slotScopeIds + ); + traverseStaticChildren(n1, n2, false); + } else if (!optimized) { + patchChildren( + n1, + n2, + currentContainer, + currentAnchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + false + ); + } + if (disabled) { + if (!wasDisabled) { + moveTeleport( + n2, + container, + mainAnchor, + internals, + 1 + ); + } else { + if (n2.props && n1.props && n2.props.to !== n1.props.to) { + n2.props.to = n1.props.to; + } + } + } else { + if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) { + const nextTarget = n2.target = resolveTarget( + n2.props, + querySelector + ); + if (nextTarget) { + moveTeleport( + n2, + nextTarget, + null, + internals, + 0 + ); + } else if (true) { + warn$1( + "Invalid Teleport target on update:", + target, + `(${typeof target})` + ); + } + } else if (wasDisabled) { + moveTeleport( + n2, + target, + targetAnchor, + internals, + 1 + ); + } + } + updateCssVars(n2, disabled); + } + }, + remove(vnode, parentComponent, parentSuspense, { um: unmount, o: { remove: hostRemove } }, doRemove) { + const { + shapeFlag, + children, + anchor, + targetStart, + targetAnchor, + target, + props + } = vnode; + if (target) { + hostRemove(targetStart); + hostRemove(targetAnchor); + } + doRemove && hostRemove(anchor); + if (shapeFlag & 16) { + const shouldRemove = doRemove || !isTeleportDisabled(props); + for (let i = 0; i < children.length; i++) { + const child = children[i]; + unmount( + child, + parentComponent, + parentSuspense, + shouldRemove, + !!child.dynamicChildren + ); + } + } + }, + move: moveTeleport, + hydrate: hydrateTeleport +}; +function moveTeleport(vnode, container, parentAnchor, { o: { insert }, m: move }, moveType = 2) { + if (moveType === 0) { + insert(vnode.targetAnchor, container, parentAnchor); + } + const { el, anchor, shapeFlag, children, props } = vnode; + const isReorder = moveType === 2; + if (isReorder) { + insert(el, container, parentAnchor); + } + if (!isReorder || isTeleportDisabled(props)) { + if (shapeFlag & 16) { + for (let i = 0; i < children.length; i++) { + move( + children[i], + container, + parentAnchor, + 2 + ); + } + } + } + if (isReorder) { + insert(anchor, container, parentAnchor); + } +} +function hydrateTeleport(node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized, { + o: { nextSibling, parentNode, querySelector, insert, createText } +}, hydrateChildren) { + function hydrateAnchor(target2, targetNode) { + let targetAnchor = targetNode; + while (targetAnchor) { + if (targetAnchor && targetAnchor.nodeType === 8) { + if (targetAnchor.data === "teleport start anchor") { + vnode.targetStart = targetAnchor; + } else if (targetAnchor.data === "teleport anchor") { + vnode.targetAnchor = targetAnchor; + target2._lpa = vnode.targetAnchor && nextSibling(vnode.targetAnchor); + break; + } + } + targetAnchor = nextSibling(targetAnchor); + } + } + function hydrateDisabledTeleport(node2, vnode2) { + vnode2.anchor = hydrateChildren( + nextSibling(node2), + vnode2, + parentNode(node2), + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + const target = vnode.target = resolveTarget( + vnode.props, + querySelector + ); + const disabled = isTeleportDisabled(vnode.props); + if (target) { + const targetNode = target._lpa || target.firstChild; + if (vnode.shapeFlag & 16) { + if (disabled) { + hydrateDisabledTeleport(node, vnode); + hydrateAnchor(target, targetNode); + if (!vnode.targetAnchor) { + prepareAnchor( + target, + vnode, + createText, + insert, + // if target is the same as the main view, insert anchors before current node + // to avoid hydrating mismatch + parentNode(node) === target ? node : null + ); + } + } else { + vnode.anchor = nextSibling(node); + hydrateAnchor(target, targetNode); + if (!vnode.targetAnchor) { + prepareAnchor(target, vnode, createText, insert); + } + hydrateChildren( + targetNode && nextSibling(targetNode), + vnode, + target, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } + updateCssVars(vnode, disabled); + } else if (disabled) { + if (vnode.shapeFlag & 16) { + hydrateDisabledTeleport(node, vnode); + vnode.targetStart = node; + vnode.targetAnchor = nextSibling(node); + } + } + return vnode.anchor && nextSibling(vnode.anchor); +} +var Teleport = TeleportImpl; +function updateCssVars(vnode, isDisabled) { + const ctx = vnode.ctx; + if (ctx && ctx.ut) { + let node, anchor; + if (isDisabled) { + node = vnode.el; + anchor = vnode.anchor; + } else { + node = vnode.targetStart; + anchor = vnode.targetAnchor; + } + while (node && node !== anchor) { + if (node.nodeType === 1) node.setAttribute("data-v-owner", ctx.uid); + node = node.nextSibling; + } + ctx.ut(); + } +} +function prepareAnchor(target, vnode, createText, insert, anchor = null) { + const targetStart = vnode.targetStart = createText(""); + const targetAnchor = vnode.targetAnchor = createText(""); + targetStart[TeleportEndKey] = targetAnchor; + if (target) { + insert(targetStart, target, anchor); + insert(targetAnchor, target, anchor); + } + return targetAnchor; +} +var leaveCbKey = /* @__PURE__ */ Symbol("_leaveCb"); +var enterCbKey = /* @__PURE__ */ Symbol("_enterCb"); +function useTransitionState() { + const state = { + isMounted: false, + isLeaving: false, + isUnmounting: false, + leavingVNodes: /* @__PURE__ */ new Map() + }; + onMounted(() => { + state.isMounted = true; + }); + onBeforeUnmount(() => { + state.isUnmounting = true; + }); + return state; +} +var TransitionHookValidator = [Function, Array]; +var BaseTransitionPropsValidators = { + mode: String, + appear: Boolean, + persisted: Boolean, + // enter + onBeforeEnter: TransitionHookValidator, + onEnter: TransitionHookValidator, + onAfterEnter: TransitionHookValidator, + onEnterCancelled: TransitionHookValidator, + // leave + onBeforeLeave: TransitionHookValidator, + onLeave: TransitionHookValidator, + onAfterLeave: TransitionHookValidator, + onLeaveCancelled: TransitionHookValidator, + // appear + onBeforeAppear: TransitionHookValidator, + onAppear: TransitionHookValidator, + onAfterAppear: TransitionHookValidator, + onAppearCancelled: TransitionHookValidator +}; +var recursiveGetSubtree = (instance) => { + const subTree = instance.subTree; + return subTree.component ? recursiveGetSubtree(subTree.component) : subTree; +}; +var BaseTransitionImpl = { + name: `BaseTransition`, + props: BaseTransitionPropsValidators, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const state = useTransitionState(); + return () => { + const children = slots.default && getTransitionRawChildren(slots.default(), true); + if (!children || !children.length) { + return; + } + const child = findNonCommentChild(children); + const rawProps = toRaw(props); + const { mode } = rawProps; + if (mode && mode !== "in-out" && mode !== "out-in" && mode !== "default") { + warn$1(`invalid mode: ${mode}`); + } + if (state.isLeaving) { + return emptyPlaceholder(child); + } + const innerChild = getInnerChild$1(child); + if (!innerChild) { + return emptyPlaceholder(child); + } + let enterHooks = resolveTransitionHooks( + innerChild, + rawProps, + state, + instance, + // #11061, ensure enterHooks is fresh after clone + (hooks) => enterHooks = hooks + ); + if (innerChild.type !== Comment) { + setTransitionHooks(innerChild, enterHooks); + } + let oldInnerChild = instance.subTree && getInnerChild$1(instance.subTree); + if (oldInnerChild && oldInnerChild.type !== Comment && !isSameVNodeType(oldInnerChild, innerChild) && recursiveGetSubtree(instance).type !== Comment) { + let leavingHooks = resolveTransitionHooks( + oldInnerChild, + rawProps, + state, + instance + ); + setTransitionHooks(oldInnerChild, leavingHooks); + if (mode === "out-in" && innerChild.type !== Comment) { + state.isLeaving = true; + leavingHooks.afterLeave = () => { + state.isLeaving = false; + if (!(instance.job.flags & 8)) { + instance.update(); + } + delete leavingHooks.afterLeave; + oldInnerChild = void 0; + }; + return emptyPlaceholder(child); + } else if (mode === "in-out" && innerChild.type !== Comment) { + leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => { + const leavingVNodesCache = getLeavingNodesForType( + state, + oldInnerChild + ); + leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild; + el[leaveCbKey] = () => { + earlyRemove(); + el[leaveCbKey] = void 0; + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + enterHooks.delayedLeave = () => { + delayedLeave(); + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + }; + } else { + oldInnerChild = void 0; + } + } else if (oldInnerChild) { + oldInnerChild = void 0; + } + return child; + }; + } +}; +function findNonCommentChild(children) { + let child = children[0]; + if (children.length > 1) { + let hasFound = false; + for (const c of children) { + if (c.type !== Comment) { + if (hasFound) { + warn$1( + " can only be used on a single element or component. Use for lists." + ); + break; + } + child = c; + hasFound = true; + if (false) break; + } + } + } + return child; +} +var BaseTransition = BaseTransitionImpl; +function getLeavingNodesForType(state, vnode) { + const { leavingVNodes } = state; + let leavingVNodesCache = leavingVNodes.get(vnode.type); + if (!leavingVNodesCache) { + leavingVNodesCache = /* @__PURE__ */ Object.create(null); + leavingVNodes.set(vnode.type, leavingVNodesCache); + } + return leavingVNodesCache; +} +function resolveTransitionHooks(vnode, props, state, instance, postClone) { + const { + appear, + mode, + persisted = false, + onBeforeEnter, + onEnter, + onAfterEnter, + onEnterCancelled, + onBeforeLeave, + onLeave, + onAfterLeave, + onLeaveCancelled, + onBeforeAppear, + onAppear, + onAfterAppear, + onAppearCancelled + } = props; + const key = String(vnode.key); + const leavingVNodesCache = getLeavingNodesForType(state, vnode); + const callHook3 = (hook, args) => { + hook && callWithAsyncErrorHandling( + hook, + instance, + 9, + args + ); + }; + const callAsyncHook = (hook, args) => { + const done = args[1]; + callHook3(hook, args); + if (isArray(hook)) { + if (hook.every((hook2) => hook2.length <= 1)) done(); + } else if (hook.length <= 1) { + done(); + } + }; + const hooks = { + mode, + persisted, + beforeEnter(el) { + let hook = onBeforeEnter; + if (!state.isMounted) { + if (appear) { + hook = onBeforeAppear || onBeforeEnter; + } else { + return; + } + } + if (el[leaveCbKey]) { + el[leaveCbKey]( + true + /* cancelled */ + ); + } + const leavingVNode = leavingVNodesCache[key]; + if (leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el[leaveCbKey]) { + leavingVNode.el[leaveCbKey](); + } + callHook3(hook, [el]); + }, + enter(el) { + let hook = onEnter; + let afterHook = onAfterEnter; + let cancelHook = onEnterCancelled; + if (!state.isMounted) { + if (appear) { + hook = onAppear || onEnter; + afterHook = onAfterAppear || onAfterEnter; + cancelHook = onAppearCancelled || onEnterCancelled; + } else { + return; + } + } + let called = false; + el[enterCbKey] = (cancelled) => { + if (called) return; + called = true; + if (cancelled) { + callHook3(cancelHook, [el]); + } else { + callHook3(afterHook, [el]); + } + if (hooks.delayedLeave) { + hooks.delayedLeave(); + } + el[enterCbKey] = void 0; + }; + const done = el[enterCbKey].bind(null, false); + if (hook) { + callAsyncHook(hook, [el, done]); + } else { + done(); + } + }, + leave(el, remove2) { + const key2 = String(vnode.key); + if (el[enterCbKey]) { + el[enterCbKey]( + true + /* cancelled */ + ); + } + if (state.isUnmounting) { + return remove2(); + } + callHook3(onBeforeLeave, [el]); + let called = false; + el[leaveCbKey] = (cancelled) => { + if (called) return; + called = true; + remove2(); + if (cancelled) { + callHook3(onLeaveCancelled, [el]); + } else { + callHook3(onAfterLeave, [el]); + } + el[leaveCbKey] = void 0; + if (leavingVNodesCache[key2] === vnode) { + delete leavingVNodesCache[key2]; + } + }; + const done = el[leaveCbKey].bind(null, false); + leavingVNodesCache[key2] = vnode; + if (onLeave) { + callAsyncHook(onLeave, [el, done]); + } else { + done(); + } + }, + clone(vnode2) { + const hooks2 = resolveTransitionHooks( + vnode2, + props, + state, + instance, + postClone + ); + if (postClone) postClone(hooks2); + return hooks2; + } + }; + return hooks; +} +function emptyPlaceholder(vnode) { + if (isKeepAlive(vnode)) { + vnode = cloneVNode(vnode); + vnode.children = null; + return vnode; + } +} +function getInnerChild$1(vnode) { + if (!isKeepAlive(vnode)) { + if (isTeleport(vnode.type) && vnode.children) { + return findNonCommentChild(vnode.children); + } + return vnode; + } + if (vnode.component) { + return vnode.component.subTree; + } + const { shapeFlag, children } = vnode; + if (children) { + if (shapeFlag & 16) { + return children[0]; + } + if (shapeFlag & 32 && isFunction(children.default)) { + return children.default(); + } + } +} +function setTransitionHooks(vnode, hooks) { + if (vnode.shapeFlag & 6 && vnode.component) { + vnode.transition = hooks; + setTransitionHooks(vnode.component.subTree, hooks); + } else if (vnode.shapeFlag & 128) { + vnode.ssContent.transition = hooks.clone(vnode.ssContent); + vnode.ssFallback.transition = hooks.clone(vnode.ssFallback); + } else { + vnode.transition = hooks; + } +} +function getTransitionRawChildren(children, keepComment = false, parentKey) { + let ret = []; + let keyedFragmentCount = 0; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + const key = parentKey == null ? child.key : String(parentKey) + String(child.key != null ? child.key : i); + if (child.type === Fragment) { + if (child.patchFlag & 128) keyedFragmentCount++; + ret = ret.concat( + getTransitionRawChildren(child.children, keepComment, key) + ); + } else if (keepComment || child.type !== Comment) { + ret.push(key != null ? cloneVNode(child, { key }) : child); + } + } + if (keyedFragmentCount > 1) { + for (let i = 0; i < ret.length; i++) { + ret[i].patchFlag = -2; + } + } + return ret; +} +function defineComponent(options, extraOptions) { + return isFunction(options) ? ( + // #8236: extend call and options.name access are considered side-effects + // by Rollup, so we have to wrap it in a pure-annotated IIFE. + (() => extend({ name: options.name }, extraOptions, { setup: options }))() + ) : options; +} +function useId() { + const i = getCurrentInstance(); + if (i) { + return (i.appContext.config.idPrefix || "v") + "-" + i.ids[0] + i.ids[1]++; + } else if (true) { + warn$1( + `useId() is called when there is no active component instance to be associated with.` + ); + } + return ""; +} +function markAsyncBoundary(instance) { + instance.ids = [instance.ids[0] + instance.ids[2]++ + "-", 0, 0]; +} +var knownTemplateRefs = /* @__PURE__ */ new WeakSet(); +function useTemplateRef(key) { + const i = getCurrentInstance(); + const r = shallowRef(null); + if (i) { + const refs = i.refs === EMPTY_OBJ ? i.refs = {} : i.refs; + if (isTemplateRefKey(refs, key)) { + warn$1(`useTemplateRef('${key}') already exists.`); + } else { + Object.defineProperty(refs, key, { + enumerable: true, + get: () => r.value, + set: (val) => r.value = val + }); + } + } else if (true) { + warn$1( + `useTemplateRef() is called when there is no active component instance to be associated with.` + ); + } + const ret = true ? readonly(r) : r; + if (true) { + knownTemplateRefs.add(ret); + } + return ret; +} +function isTemplateRefKey(refs, key) { + let desc; + return !!((desc = Object.getOwnPropertyDescriptor(refs, key)) && !desc.configurable); +} +var pendingSetRefMap = /* @__PURE__ */ new WeakMap(); +function setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = false) { + if (isArray(rawRef)) { + rawRef.forEach( + (r, i) => setRef( + r, + oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), + parentSuspense, + vnode, + isUnmount + ) + ); + return; + } + if (isAsyncWrapper(vnode) && !isUnmount) { + if (vnode.shapeFlag & 512 && vnode.type.__asyncResolved && vnode.component.subTree.component) { + setRef(rawRef, oldRawRef, parentSuspense, vnode.component.subTree); + } + return; + } + const refValue = vnode.shapeFlag & 4 ? getComponentPublicInstance(vnode.component) : vnode.el; + const value = isUnmount ? null : refValue; + const { i: owner, r: ref2 } = rawRef; + if (!owner) { + warn$1( + `Missing ref owner context. ref cannot be used on hoisted vnodes. A vnode with ref must be created inside the render function.` + ); + return; + } + const oldRef = oldRawRef && oldRawRef.r; + const refs = owner.refs === EMPTY_OBJ ? owner.refs = {} : owner.refs; + const setupState = owner.setupState; + const rawSetupState = toRaw(setupState); + const canSetSetupRef = setupState === EMPTY_OBJ ? NO : (key) => { + if (true) { + if (hasOwn(rawSetupState, key) && !isRef2(rawSetupState[key])) { + warn$1( + `Template ref "${key}" used on a non-ref value. It will not work in the production build.` + ); + } + if (knownTemplateRefs.has(rawSetupState[key])) { + return false; + } + } + if (isTemplateRefKey(refs, key)) { + return false; + } + return hasOwn(rawSetupState, key); + }; + const canSetRef = (ref22, key) => { + if (knownTemplateRefs.has(ref22)) { + return false; + } + if (key && isTemplateRefKey(refs, key)) { + return false; + } + return true; + }; + if (oldRef != null && oldRef !== ref2) { + invalidatePendingSetRef(oldRawRef); + if (isString(oldRef)) { + refs[oldRef] = null; + if (canSetSetupRef(oldRef)) { + setupState[oldRef] = null; + } + } else if (isRef2(oldRef)) { + const oldRawRefAtom = oldRawRef; + if (canSetRef(oldRef, oldRawRefAtom.k)) { + oldRef.value = null; + } + if (oldRawRefAtom.k) refs[oldRawRefAtom.k] = null; + } + } + if (isFunction(ref2)) { + callWithErrorHandling(ref2, owner, 12, [value, refs]); + } else { + const _isString = isString(ref2); + const _isRef = isRef2(ref2); + if (_isString || _isRef) { + const doSet = () => { + if (rawRef.f) { + const existing = _isString ? canSetSetupRef(ref2) ? setupState[ref2] : refs[ref2] : canSetRef(ref2) || !rawRef.k ? ref2.value : refs[rawRef.k]; + if (isUnmount) { + isArray(existing) && remove(existing, refValue); + } else { + if (!isArray(existing)) { + if (_isString) { + refs[ref2] = [refValue]; + if (canSetSetupRef(ref2)) { + setupState[ref2] = refs[ref2]; + } + } else { + const newVal = [refValue]; + if (canSetRef(ref2, rawRef.k)) { + ref2.value = newVal; + } + if (rawRef.k) refs[rawRef.k] = newVal; + } + } else if (!existing.includes(refValue)) { + existing.push(refValue); + } + } + } else if (_isString) { + refs[ref2] = value; + if (canSetSetupRef(ref2)) { + setupState[ref2] = value; + } + } else if (_isRef) { + if (canSetRef(ref2, rawRef.k)) { + ref2.value = value; + } + if (rawRef.k) refs[rawRef.k] = value; + } else if (true) { + warn$1("Invalid template ref type:", ref2, `(${typeof ref2})`); + } + }; + if (value) { + const job = () => { + doSet(); + pendingSetRefMap.delete(rawRef); + }; + job.id = -1; + pendingSetRefMap.set(rawRef, job); + queuePostRenderEffect(job, parentSuspense); + } else { + invalidatePendingSetRef(rawRef); + doSet(); + } + } else if (true) { + warn$1("Invalid template ref type:", ref2, `(${typeof ref2})`); + } + } +} +function invalidatePendingSetRef(rawRef) { + const pendingSetRef = pendingSetRefMap.get(rawRef); + if (pendingSetRef) { + pendingSetRef.flags |= 8; + pendingSetRefMap.delete(rawRef); + } +} +var hasLoggedMismatchError = false; +var logMismatchError = () => { + if (hasLoggedMismatchError) { + return; + } + console.error("Hydration completed but contains mismatches."); + hasLoggedMismatchError = true; +}; +var isSVGContainer = (container) => container.namespaceURI.includes("svg") && container.tagName !== "foreignObject"; +var isMathMLContainer = (container) => container.namespaceURI.includes("MathML"); +var getContainerType = (container) => { + if (container.nodeType !== 1) return void 0; + if (isSVGContainer(container)) return "svg"; + if (isMathMLContainer(container)) return "mathml"; + return void 0; +}; +var isComment = (node) => node.nodeType === 8; +function createHydrationFunctions(rendererInternals) { + const { + mt: mountComponent, + p: patch, + o: { + patchProp: patchProp2, + createText, + nextSibling, + parentNode, + remove: remove2, + insert, + createComment + } + } = rendererInternals; + const hydrate2 = (vnode, container) => { + if (!container.hasChildNodes()) { + warn$1( + `Attempting to hydrate existing markup but container is empty. Performing full mount instead.` + ); + patch(null, vnode, container); + flushPostFlushCbs(); + container._vnode = vnode; + return; + } + hydrateNode(container.firstChild, vnode, null, null, null); + flushPostFlushCbs(); + container._vnode = vnode; + }; + const hydrateNode = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized = false) => { + optimized = optimized || !!vnode.dynamicChildren; + const isFragmentStart = isComment(node) && node.data === "["; + const onMismatch = () => handleMismatch( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + isFragmentStart + ); + const { type, ref: ref2, shapeFlag, patchFlag } = vnode; + let domType = node.nodeType; + vnode.el = node; + if (true) { + def(node, "__vnode", vnode, true); + def(node, "__vueParentComponent", parentComponent, true); + } + if (patchFlag === -2) { + optimized = false; + vnode.dynamicChildren = null; + } + let nextNode = null; + switch (type) { + case Text: + if (domType !== 3) { + if (vnode.children === "") { + insert(vnode.el = createText(""), parentNode(node), node); + nextNode = node; + } else { + nextNode = onMismatch(); + } + } else { + if (node.data !== vnode.children) { + warn$1( + `Hydration text mismatch in`, + node.parentNode, + ` + - rendered on server: ${JSON.stringify( + node.data + )} + - expected on client: ${JSON.stringify(vnode.children)}` + ); + logMismatchError(); + node.data = vnode.children; + } + nextNode = nextSibling(node); + } + break; + case Comment: + if (isTemplateNode(node)) { + nextNode = nextSibling(node); + replaceNode( + vnode.el = node.content.firstChild, + node, + parentComponent + ); + } else if (domType !== 8 || isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = nextSibling(node); + } + break; + case Static: + if (isFragmentStart) { + node = nextSibling(node); + domType = node.nodeType; + } + if (domType === 1 || domType === 3) { + nextNode = node; + const needToAdoptContent = !vnode.children.length; + for (let i = 0; i < vnode.staticCount; i++) { + if (needToAdoptContent) + vnode.children += nextNode.nodeType === 1 ? nextNode.outerHTML : nextNode.data; + if (i === vnode.staticCount - 1) { + vnode.anchor = nextNode; + } + nextNode = nextSibling(nextNode); + } + return isFragmentStart ? nextSibling(nextNode) : nextNode; + } else { + onMismatch(); + } + break; + case Fragment: + if (!isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = hydrateFragment( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + break; + default: + if (shapeFlag & 1) { + if ((domType !== 1 || vnode.type.toLowerCase() !== node.tagName.toLowerCase()) && !isTemplateNode(node)) { + nextNode = onMismatch(); + } else { + nextNode = hydrateElement( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } else if (shapeFlag & 6) { + vnode.slotScopeIds = slotScopeIds; + const container = parentNode(node); + if (isFragmentStart) { + nextNode = locateClosingAnchor(node); + } else if (isComment(node) && node.data === "teleport start") { + nextNode = locateClosingAnchor(node, node.data, "teleport end"); + } else { + nextNode = nextSibling(node); + } + mountComponent( + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + optimized + ); + if (isAsyncWrapper(vnode) && !vnode.type.__asyncResolved) { + let subTree; + if (isFragmentStart) { + subTree = createVNode(Fragment); + subTree.anchor = nextNode ? nextNode.previousSibling : container.lastChild; + } else { + subTree = node.nodeType === 3 ? createTextVNode("") : createVNode("div"); + } + subTree.el = node; + vnode.component.subTree = subTree; + } + } else if (shapeFlag & 64) { + if (domType !== 8) { + nextNode = onMismatch(); + } else { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized, + rendererInternals, + hydrateChildren + ); + } + } else if (shapeFlag & 128) { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + getContainerType(parentNode(node)), + slotScopeIds, + optimized, + rendererInternals, + hydrateNode + ); + } else if (true) { + warn$1("Invalid HostVNode type:", type, `(${typeof type})`); + } + } + if (ref2 != null) { + setRef(ref2, null, parentSuspense, vnode); + } + return nextNode; + }; + const hydrateElement = (el, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!vnode.dynamicChildren; + const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode; + const forcePatch = type === "input" || type === "option"; + if (true) { + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "created"); + } + let needCallTransitionHooks = false; + if (isTemplateNode(el)) { + needCallTransitionHooks = needTransition( + null, + // no need check parentSuspense in hydration + transition + ) && parentComponent && parentComponent.vnode.props && parentComponent.vnode.props.appear; + const content = el.content.firstChild; + if (needCallTransitionHooks) { + const cls = content.getAttribute("class"); + if (cls) content.$cls = cls; + transition.beforeEnter(content); + } + replaceNode(content, el, parentComponent); + vnode.el = el = content; + } + if (shapeFlag & 16 && // skip if element has innerHTML / textContent + !(props && (props.innerHTML || props.textContent))) { + let next = hydrateChildren( + el.firstChild, + vnode, + el, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + let hasWarned2 = false; + while (next) { + if (!isMismatchAllowed( + el, + 1 + /* CHILDREN */ + )) { + if (!hasWarned2) { + warn$1( + `Hydration children mismatch on`, + el, + ` +Server rendered element contains more child nodes than client vdom.` + ); + hasWarned2 = true; + } + logMismatchError(); + } + const cur = next; + next = next.nextSibling; + remove2(cur); + } + } else if (shapeFlag & 8) { + let clientText = vnode.children; + if (clientText[0] === "\n" && (el.tagName === "PRE" || el.tagName === "TEXTAREA")) { + clientText = clientText.slice(1); + } + const { textContent } = el; + if (textContent !== clientText && // innerHTML normalize \r\n or \r into a single \n in the DOM + textContent !== clientText.replace(/\r\n|\r/g, "\n")) { + if (!isMismatchAllowed( + el, + 0 + /* TEXT */ + )) { + warn$1( + `Hydration text content mismatch on`, + el, + ` + - rendered on server: ${textContent} + - expected on client: ${clientText}` + ); + logMismatchError(); + } + el.textContent = vnode.children; + } + } + if (props) { + if (true) { + const isCustomElement = el.tagName.includes("-"); + for (const key in props) { + if (// #11189 skip if this node has directives that have created hooks + // as it could have mutated the DOM in any possible way + !(dirs && dirs.some((d) => d.dir.created)) && propHasMismatch(el, key, props[key], vnode, parentComponent)) { + logMismatchError(); + } + if (forcePatch && (key.endsWith("value") || key === "indeterminate") || isOn(key) && !isReservedProp(key) || // force hydrate v-bind with .prop modifiers + key[0] === "." || isCustomElement && !isReservedProp(key)) { + patchProp2(el, key, null, props[key], void 0, parentComponent); + } + } + } else if (props.onClick) { + patchProp2( + el, + "onClick", + null, + props.onClick, + void 0, + parentComponent + ); + } else if (patchFlag & 4 && isReactive(props.style)) { + for (const key in props.style) props.style[key]; + } + } + let vnodeHooks; + if (vnodeHooks = props && props.onVnodeBeforeMount) { + invokeVNodeHook(vnodeHooks, parentComponent, vnode); + } + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "beforeMount"); + } + if ((vnodeHooks = props && props.onVnodeMounted) || dirs || needCallTransitionHooks) { + queueEffectWithSuspense(() => { + vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode); + needCallTransitionHooks && transition.enter(el); + dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted"); + }, parentSuspense); + } + } + return el.nextSibling; + }; + const hydrateChildren = (node, parentVNode, container, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!parentVNode.dynamicChildren; + const children = parentVNode.children; + const l = children.length; + let hasWarned2 = false; + for (let i = 0; i < l; i++) { + const vnode = optimized ? children[i] : children[i] = normalizeVNode(children[i]); + const isText = vnode.type === Text; + if (node) { + if (isText && !optimized) { + if (i + 1 < l && normalizeVNode(children[i + 1]).type === Text) { + insert( + createText( + node.data.slice(vnode.children.length) + ), + container, + nextSibling(node) + ); + node.data = vnode.children; + } + } + node = hydrateNode( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } else if (isText && !vnode.children) { + insert(vnode.el = createText(""), container); + } else { + if (!isMismatchAllowed( + container, + 1 + /* CHILDREN */ + )) { + if (!hasWarned2) { + warn$1( + `Hydration children mismatch on`, + container, + ` +Server rendered element contains fewer child nodes than client vdom.` + ); + hasWarned2 = true; + } + logMismatchError(); + } + patch( + null, + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + } + } + return node; + }; + const hydrateFragment = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + const { slotScopeIds: fragmentSlotScopeIds } = vnode; + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds; + } + const container = parentNode(node); + const next = hydrateChildren( + nextSibling(node), + vnode, + container, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + if (next && isComment(next) && next.data === "]") { + return nextSibling(vnode.anchor = next); + } else { + logMismatchError(); + insert(vnode.anchor = createComment(`]`), container, next); + return next; + } + }; + const handleMismatch = (node, vnode, parentComponent, parentSuspense, slotScopeIds, isFragment) => { + if (!isMismatchAllowed( + node.parentElement, + 1 + /* CHILDREN */ + )) { + warn$1( + `Hydration node mismatch: +- rendered on server:`, + node, + node.nodeType === 3 ? `(text)` : isComment(node) && node.data === "[" ? `(start of fragment)` : ``, + ` +- expected on client:`, + vnode.type + ); + logMismatchError(); + } + vnode.el = null; + if (isFragment) { + const end = locateClosingAnchor(node); + while (true) { + const next2 = nextSibling(node); + if (next2 && next2 !== end) { + remove2(next2); + } else { + break; + } + } + } + const next = nextSibling(node); + const container = parentNode(node); + remove2(node); + patch( + null, + vnode, + container, + next, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + if (parentComponent) { + parentComponent.vnode.el = vnode.el; + updateHOCHostEl(parentComponent, vnode.el); + } + return next; + }; + const locateClosingAnchor = (node, open = "[", close = "]") => { + let match = 0; + while (node) { + node = nextSibling(node); + if (node && isComment(node)) { + if (node.data === open) match++; + if (node.data === close) { + if (match === 0) { + return nextSibling(node); + } else { + match--; + } + } + } + } + return node; + }; + const replaceNode = (newNode, oldNode, parentComponent) => { + const parentNode2 = oldNode.parentNode; + if (parentNode2) { + parentNode2.replaceChild(newNode, oldNode); + } + let parent = parentComponent; + while (parent) { + if (parent.vnode.el === oldNode) { + parent.vnode.el = parent.subTree.el = newNode; + } + parent = parent.parent; + } + }; + const isTemplateNode = (node) => { + return node.nodeType === 1 && node.tagName === "TEMPLATE"; + }; + return [hydrate2, hydrateNode]; +} +function propHasMismatch(el, key, clientValue, vnode, instance) { + let mismatchType; + let mismatchKey; + let actual; + let expected; + if (key === "class") { + if (el.$cls) { + actual = el.$cls; + delete el.$cls; + } else { + actual = el.getAttribute("class"); + } + expected = normalizeClass(clientValue); + if (!isSetEqual(toClassSet(actual || ""), toClassSet(expected))) { + mismatchType = 2; + mismatchKey = `class`; + } + } else if (key === "style") { + actual = el.getAttribute("style") || ""; + expected = isString(clientValue) ? clientValue : stringifyStyle(normalizeStyle(clientValue)); + const actualMap = toStyleMap(actual); + const expectedMap = toStyleMap(expected); + if (vnode.dirs) { + for (const { dir, value } of vnode.dirs) { + if (dir.name === "show" && !value) { + expectedMap.set("display", "none"); + } + } + } + if (instance) { + resolveCssVars(instance, vnode, expectedMap); + } + if (!isMapEqual(actualMap, expectedMap)) { + mismatchType = 3; + mismatchKey = "style"; + } + } else if (el instanceof SVGElement && isKnownSvgAttr(key) || el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) { + if (isBooleanAttr(key)) { + actual = el.hasAttribute(key); + expected = includeBooleanAttr(clientValue); + } else if (clientValue == null) { + actual = el.hasAttribute(key); + expected = false; + } else { + if (el.hasAttribute(key)) { + actual = el.getAttribute(key); + } else if (key === "value" && el.tagName === "TEXTAREA") { + actual = el.value; + } else { + actual = false; + } + expected = isRenderableAttrValue(clientValue) ? String(clientValue) : false; + } + if (actual !== expected) { + mismatchType = 4; + mismatchKey = key; + } + } + if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) { + const format = (v) => v === false ? `(not rendered)` : `${mismatchKey}="${v}"`; + const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`; + const postSegment = ` + - rendered on server: ${format(actual)} + - expected on client: ${format(expected)} + Note: this mismatch is check-only. The DOM will not be rectified in production due to performance overhead. + You should fix the source of the mismatch.`; + { + warn$1(preSegment, el, postSegment); + } + return true; + } + return false; +} +function toClassSet(str) { + return new Set(str.trim().split(/\s+/)); +} +function isSetEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const s of a) { + if (!b.has(s)) { + return false; + } + } + return true; +} +function toStyleMap(str) { + const styleMap = /* @__PURE__ */ new Map(); + for (const item of str.split(";")) { + let [key, value] = item.split(":"); + key = key.trim(); + value = value && value.trim(); + if (key && value) { + styleMap.set(key, value); + } + } + return styleMap; +} +function isMapEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const [key, value] of a) { + if (value !== b.get(key)) { + return false; + } + } + return true; +} +function resolveCssVars(instance, vnode, expectedMap) { + const root = instance.subTree; + if (instance.getCssVars && (vnode === root || root && root.type === Fragment && root.children.includes(vnode))) { + const cssVars = instance.getCssVars(); + for (const key in cssVars) { + const value = normalizeCssVarValue(cssVars[key]); + expectedMap.set(`--${getEscapedCssVarName(key, false)}`, value); + } + } + if (vnode === root && instance.parent) { + resolveCssVars(instance.parent, instance.vnode, expectedMap); + } +} +var allowMismatchAttr = "data-allow-mismatch"; +var MismatchTypeString = { + [ + 0 + /* TEXT */ + ]: "text", + [ + 1 + /* CHILDREN */ + ]: "children", + [ + 2 + /* CLASS */ + ]: "class", + [ + 3 + /* STYLE */ + ]: "style", + [ + 4 + /* ATTRIBUTE */ + ]: "attribute" +}; +function isMismatchAllowed(el, allowedType) { + if (allowedType === 0 || allowedType === 1) { + while (el && !el.hasAttribute(allowMismatchAttr)) { + el = el.parentElement; + } + } + const allowedAttr = el && el.getAttribute(allowMismatchAttr); + if (allowedAttr == null) { + return false; + } else if (allowedAttr === "") { + return true; + } else { + const list = allowedAttr.split(","); + if (allowedType === 0 && list.includes("children")) { + return true; + } + return list.includes(MismatchTypeString[allowedType]); + } +} +var requestIdleCallback = getGlobalThis().requestIdleCallback || ((cb) => setTimeout(cb, 1)); +var cancelIdleCallback = getGlobalThis().cancelIdleCallback || ((id) => clearTimeout(id)); +var hydrateOnIdle = (timeout = 1e4) => (hydrate2) => { + const id = requestIdleCallback(hydrate2, { timeout }); + return () => cancelIdleCallback(id); +}; +function elementIsVisibleInViewport(el) { + const { top, left, bottom, right } = el.getBoundingClientRect(); + const { innerHeight, innerWidth } = window; + return (top > 0 && top < innerHeight || bottom > 0 && bottom < innerHeight) && (left > 0 && left < innerWidth || right > 0 && right < innerWidth); +} +var hydrateOnVisible = (opts) => (hydrate2, forEach) => { + const ob = new IntersectionObserver((entries) => { + for (const e of entries) { + if (!e.isIntersecting) continue; + ob.disconnect(); + hydrate2(); + break; + } + }, opts); + forEach((el) => { + if (!(el instanceof Element)) return; + if (elementIsVisibleInViewport(el)) { + hydrate2(); + ob.disconnect(); + return false; + } + ob.observe(el); + }); + return () => ob.disconnect(); +}; +var hydrateOnMediaQuery = (query) => (hydrate2) => { + if (query) { + const mql = matchMedia(query); + if (mql.matches) { + hydrate2(); + } else { + mql.addEventListener("change", hydrate2, { once: true }); + return () => mql.removeEventListener("change", hydrate2); + } + } +}; +var hydrateOnInteraction = (interactions = []) => (hydrate2, forEach) => { + if (isString(interactions)) interactions = [interactions]; + let hasHydrated = false; + const doHydrate = (e) => { + if (!hasHydrated) { + hasHydrated = true; + teardown(); + hydrate2(); + e.target.dispatchEvent(new e.constructor(e.type, e)); + } + }; + const teardown = () => { + forEach((el) => { + for (const i of interactions) { + el.removeEventListener(i, doHydrate); + } + }); + }; + forEach((el) => { + for (const i of interactions) { + el.addEventListener(i, doHydrate, { once: true }); + } + }); + return teardown; +}; +function forEachElement(node, cb) { + if (isComment(node) && node.data === "[") { + let depth = 1; + let next = node.nextSibling; + while (next) { + if (next.nodeType === 1) { + const result = cb(next); + if (result === false) { + break; + } + } else if (isComment(next)) { + if (next.data === "]") { + if (--depth === 0) break; + } else if (next.data === "[") { + depth++; + } + } + next = next.nextSibling; + } + } else { + cb(node); + } +} +var isAsyncWrapper = (i) => !!i.type.__asyncLoader; +function defineAsyncComponent(source) { + if (isFunction(source)) { + source = { loader: source }; + } + const { + loader, + loadingComponent, + errorComponent, + delay = 200, + hydrate: hydrateStrategy, + timeout, + // undefined = never times out + suspensible = true, + onError: userOnError + } = source; + let pendingRequest = null; + let resolvedComp; + let retries = 0; + const retry = () => { + retries++; + pendingRequest = null; + return load(); + }; + const load = () => { + let thisRequest; + return pendingRequest || (thisRequest = pendingRequest = loader().catch((err) => { + err = err instanceof Error ? err : new Error(String(err)); + if (userOnError) { + return new Promise((resolve2, reject) => { + const userRetry = () => resolve2(retry()); + const userFail = () => reject(err); + userOnError(err, userRetry, userFail, retries + 1); + }); + } else { + throw err; + } + }).then((comp) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest; + } + if (!comp) { + warn$1( + `Async component loader resolved to undefined. If you are using retry(), make sure to return its return value.` + ); + } + if (comp && (comp.__esModule || comp[Symbol.toStringTag] === "Module")) { + comp = comp.default; + } + if (comp && !isObject(comp) && !isFunction(comp)) { + throw new Error(`Invalid async component load result: ${comp}`); + } + resolvedComp = comp; + return comp; + })); + }; + return defineComponent({ + name: "AsyncComponentWrapper", + __asyncLoader: load, + __asyncHydrate(el, instance, hydrate2) { + let patched = false; + (instance.bu || (instance.bu = [])).push(() => patched = true); + const performHydrate = () => { + if (patched) { + if (true) { + warn$1( + `Skipping lazy hydration for component '${getComponentName(resolvedComp) || resolvedComp.__file}': it was updated before lazy hydration performed.` + ); + } + return; + } + hydrate2(); + }; + const doHydrate = hydrateStrategy ? () => { + const teardown = hydrateStrategy( + performHydrate, + (cb) => forEachElement(el, cb) + ); + if (teardown) { + (instance.bum || (instance.bum = [])).push(teardown); + } + } : performHydrate; + if (resolvedComp) { + doHydrate(); + } else { + load().then(() => !instance.isUnmounted && doHydrate()); + } + }, + get __asyncResolved() { + return resolvedComp; + }, + setup() { + const instance = currentInstance; + markAsyncBoundary(instance); + if (resolvedComp) { + return () => createInnerComp(resolvedComp, instance); + } + const onError = (err) => { + pendingRequest = null; + handleError( + err, + instance, + 13, + !errorComponent + ); + }; + if (suspensible && instance.suspense || isInSSRComponentSetup) { + return load().then((comp) => { + return () => createInnerComp(comp, instance); + }).catch((err) => { + onError(err); + return () => errorComponent ? createVNode(errorComponent, { + error: err + }) : null; + }); + } + const loaded = ref(false); + const error = ref(); + const delayed = ref(!!delay); + if (delay) { + setTimeout(() => { + delayed.value = false; + }, delay); + } + if (timeout != null) { + setTimeout(() => { + if (!loaded.value && !error.value) { + const err = new Error( + `Async component timed out after ${timeout}ms.` + ); + onError(err); + error.value = err; + } + }, timeout); + } + load().then(() => { + loaded.value = true; + if (instance.parent && isKeepAlive(instance.parent.vnode)) { + instance.parent.update(); + } + }).catch((err) => { + onError(err); + error.value = err; + }); + return () => { + if (loaded.value && resolvedComp) { + return createInnerComp(resolvedComp, instance); + } else if (error.value && errorComponent) { + return createVNode(errorComponent, { + error: error.value + }); + } else if (loadingComponent && !delayed.value) { + return createInnerComp( + loadingComponent, + instance + ); + } + }; + } + }); +} +function createInnerComp(comp, parent) { + const { ref: ref2, props, children, ce } = parent.vnode; + const vnode = createVNode(comp, props, children); + vnode.ref = ref2; + vnode.ce = ce; + delete parent.vnode.ce; + return vnode; +} +var isKeepAlive = (vnode) => vnode.type.__isKeepAlive; +var KeepAliveImpl = { + name: `KeepAlive`, + // Marker for special handling inside the renderer. We are not using a === + // check directly on KeepAlive in the renderer, because importing it directly + // would prevent it from being tree-shaken. + __isKeepAlive: true, + props: { + include: [String, RegExp, Array], + exclude: [String, RegExp, Array], + max: [String, Number] + }, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const sharedContext = instance.ctx; + if (!sharedContext.renderer) { + return () => { + const children = slots.default && slots.default(); + return children && children.length === 1 ? children[0] : children; + }; + } + const cache = /* @__PURE__ */ new Map(); + const keys = /* @__PURE__ */ new Set(); + let current = null; + if (true) { + instance.__v_cache = cache; + } + const parentSuspense = instance.suspense; + const { + renderer: { + p: patch, + m: move, + um: _unmount, + o: { createElement } + } + } = sharedContext; + const storageContainer = createElement("div"); + sharedContext.activate = (vnode, container, anchor, namespace, optimized) => { + const instance2 = vnode.component; + move(vnode, container, anchor, 0, parentSuspense); + patch( + instance2.vnode, + vnode, + container, + anchor, + instance2, + parentSuspense, + namespace, + vnode.slotScopeIds, + optimized + ); + queuePostRenderEffect(() => { + instance2.isDeactivated = false; + if (instance2.a) { + invokeArrayFns(instance2.a); + } + const vnodeHook = vnode.props && vnode.props.onVnodeMounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + }, parentSuspense); + if (true) { + devtoolsComponentAdded(instance2); + } + }; + sharedContext.deactivate = (vnode) => { + const instance2 = vnode.component; + invalidateMount(instance2.m); + invalidateMount(instance2.a); + move(vnode, storageContainer, null, 1, parentSuspense); + queuePostRenderEffect(() => { + if (instance2.da) { + invokeArrayFns(instance2.da); + } + const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + instance2.isDeactivated = true; + }, parentSuspense); + if (true) { + devtoolsComponentAdded(instance2); + } + if (true) { + instance2.__keepAliveStorageContainer = storageContainer; + } + }; + function unmount(vnode) { + resetShapeFlag(vnode); + _unmount(vnode, instance, parentSuspense, true); + } + function pruneCache(filter) { + cache.forEach((vnode, key) => { + const name = getComponentName( + isAsyncWrapper(vnode) ? vnode.type.__asyncResolved || {} : vnode.type + ); + if (name && !filter(name)) { + pruneCacheEntry(key); + } + }); + } + function pruneCacheEntry(key) { + const cached = cache.get(key); + if (cached && (!current || !isSameVNodeType(cached, current))) { + unmount(cached); + } else if (current) { + resetShapeFlag(current); + } + cache.delete(key); + keys.delete(key); + } + watch2( + () => [props.include, props.exclude], + ([include, exclude]) => { + include && pruneCache((name) => matches(include, name)); + exclude && pruneCache((name) => !matches(exclude, name)); + }, + // prune post-render after `current` has been updated + { flush: "post", deep: true } + ); + let pendingCacheKey = null; + const cacheSubtree = () => { + if (pendingCacheKey != null) { + if (isSuspense(instance.subTree.type)) { + queuePostRenderEffect(() => { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + }, instance.subTree.suspense); + } else { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + } + } + }; + onMounted(cacheSubtree); + onUpdated(cacheSubtree); + onBeforeUnmount(() => { + cache.forEach((cached) => { + const { subTree, suspense } = instance; + const vnode = getInnerChild(subTree); + if (cached.type === vnode.type && cached.key === vnode.key) { + resetShapeFlag(vnode); + const da = vnode.component.da; + da && queuePostRenderEffect(da, suspense); + return; + } + unmount(cached); + }); + }); + return () => { + pendingCacheKey = null; + if (!slots.default) { + return current = null; + } + const children = slots.default(); + const rawVNode = children[0]; + if (children.length > 1) { + if (true) { + warn$1(`KeepAlive should contain exactly one component child.`); + } + current = null; + return children; + } else if (!isVNode(rawVNode) || !(rawVNode.shapeFlag & 4) && !(rawVNode.shapeFlag & 128)) { + current = null; + return rawVNode; + } + let vnode = getInnerChild(rawVNode); + if (vnode.type === Comment) { + current = null; + return vnode; + } + const comp = vnode.type; + const name = getComponentName( + isAsyncWrapper(vnode) ? vnode.type.__asyncResolved || {} : comp + ); + const { include, exclude, max } = props; + if (include && (!name || !matches(include, name)) || exclude && name && matches(exclude, name)) { + vnode.shapeFlag &= -257; + current = vnode; + return rawVNode; + } + const key = vnode.key == null ? comp : vnode.key; + const cachedVNode = cache.get(key); + if (vnode.el) { + vnode = cloneVNode(vnode); + if (rawVNode.shapeFlag & 128) { + rawVNode.ssContent = vnode; + } + } + pendingCacheKey = key; + if (cachedVNode) { + vnode.el = cachedVNode.el; + vnode.component = cachedVNode.component; + if (vnode.transition) { + setTransitionHooks(vnode, vnode.transition); + } + vnode.shapeFlag |= 512; + keys.delete(key); + keys.add(key); + } else { + keys.add(key); + if (max && keys.size > parseInt(max, 10)) { + pruneCacheEntry(keys.values().next().value); + } + } + vnode.shapeFlag |= 256; + current = vnode; + return isSuspense(rawVNode.type) ? rawVNode : vnode; + }; + } +}; +var KeepAlive = KeepAliveImpl; +function matches(pattern, name) { + if (isArray(pattern)) { + return pattern.some((p2) => matches(p2, name)); + } else if (isString(pattern)) { + return pattern.split(",").includes(name); + } else if (isRegExp(pattern)) { + pattern.lastIndex = 0; + return pattern.test(name); + } + return false; +} +function onActivated(hook, target) { + registerKeepAliveHook(hook, "a", target); +} +function onDeactivated(hook, target) { + registerKeepAliveHook(hook, "da", target); +} +function registerKeepAliveHook(hook, type, target = currentInstance) { + const wrappedHook = hook.__wdc || (hook.__wdc = () => { + let current = target; + while (current) { + if (current.isDeactivated) { + return; + } + current = current.parent; + } + return hook(); + }); + injectHook(type, wrappedHook, target); + if (target) { + let current = target.parent; + while (current && current.parent) { + if (isKeepAlive(current.parent.vnode)) { + injectToKeepAliveRoot(wrappedHook, type, target, current); + } + current = current.parent; + } + } +} +function injectToKeepAliveRoot(hook, type, target, keepAliveRoot) { + const injected = injectHook( + type, + hook, + keepAliveRoot, + true + /* prepend */ + ); + onUnmounted(() => { + remove(keepAliveRoot[type], injected); + }, target); +} +function resetShapeFlag(vnode) { + vnode.shapeFlag &= -257; + vnode.shapeFlag &= -513; +} +function getInnerChild(vnode) { + return vnode.shapeFlag & 128 ? vnode.ssContent : vnode; +} +function injectHook(type, hook, target = currentInstance, prepend = false) { + if (target) { + const hooks = target[type] || (target[type] = []); + const wrappedHook = hook.__weh || (hook.__weh = (...args) => { + pauseTracking(); + const reset = setCurrentInstance(target); + const res = callWithAsyncErrorHandling(hook, target, type, args); + reset(); + resetTracking(); + return res; + }); + if (prepend) { + hooks.unshift(wrappedHook); + } else { + hooks.push(wrappedHook); + } + return wrappedHook; + } else if (true) { + const apiName = toHandlerKey(ErrorTypeStrings$1[type].replace(/ hook$/, "")); + warn$1( + `${apiName} is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup(). If you are using async setup(), make sure to register lifecycle hooks before the first await statement.` + ); + } +} +var createHook = (lifecycle) => (hook, target = currentInstance) => { + if (!isInSSRComponentSetup || lifecycle === "sp") { + injectHook(lifecycle, (...args) => hook(...args), target); + } +}; +var onBeforeMount = createHook("bm"); +var onMounted = createHook("m"); +var onBeforeUpdate = createHook( + "bu" +); +var onUpdated = createHook("u"); +var onBeforeUnmount = createHook( + "bum" +); +var onUnmounted = createHook("um"); +var onServerPrefetch = createHook( + "sp" +); +var onRenderTriggered = createHook("rtg"); +var onRenderTracked = createHook("rtc"); +function onErrorCaptured(hook, target = currentInstance) { + injectHook("ec", hook, target); +} +var COMPONENTS = "components"; +var DIRECTIVES = "directives"; +function resolveComponent(name, maybeSelfReference) { + return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name; +} +var NULL_DYNAMIC_COMPONENT = /* @__PURE__ */ Symbol.for("v-ndc"); +function resolveDynamicComponent(component) { + if (isString(component)) { + return resolveAsset(COMPONENTS, component, false) || component; + } else { + return component || NULL_DYNAMIC_COMPONENT; + } +} +function resolveDirective(name) { + return resolveAsset(DIRECTIVES, name); +} +function resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) { + const instance = currentRenderingInstance || currentInstance; + if (instance) { + const Component = instance.type; + if (type === COMPONENTS) { + const selfName = getComponentName( + Component, + false + ); + if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) { + return Component; + } + } + const res = ( + // local registration + // check instance[type] first which is resolved for options API + resolve(instance[type] || Component[type], name) || // global registration + resolve(instance.appContext[type], name) + ); + if (!res && maybeSelfReference) { + return Component; + } + if (warnMissing && !res) { + const extra = type === COMPONENTS ? ` +If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` : ``; + warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`); + } + return res; + } else if (true) { + warn$1( + `resolve${capitalize(type.slice(0, -1))} can only be used in render() or setup().` + ); + } +} +function resolve(registry, name) { + return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]); +} +function renderList(source, renderItem, cache, index) { + let ret; + const cached = cache && cache[index]; + const sourceIsArray = isArray(source); + if (sourceIsArray || isString(source)) { + const sourceIsReactiveArray = sourceIsArray && isReactive(source); + let needsWrap = false; + let isReadonlySource = false; + if (sourceIsReactiveArray) { + needsWrap = !isShallow(source); + isReadonlySource = isReadonly(source); + source = shallowReadArray(source); + } + ret = new Array(source.length); + for (let i = 0, l = source.length; i < l; i++) { + ret[i] = renderItem( + needsWrap ? isReadonlySource ? toReadonly(toReactive(source[i])) : toReactive(source[i]) : source[i], + i, + void 0, + cached && cached[i] + ); + } + } else if (typeof source === "number") { + if (!Number.isInteger(source)) { + warn$1(`The v-for range expect an integer value but got ${source}.`); + } + ret = new Array(source); + for (let i = 0; i < source; i++) { + ret[i] = renderItem(i + 1, i, void 0, cached && cached[i]); + } + } else if (isObject(source)) { + if (source[Symbol.iterator]) { + ret = Array.from( + source, + (item, i) => renderItem(item, i, void 0, cached && cached[i]) + ); + } else { + const keys = Object.keys(source); + ret = new Array(keys.length); + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + ret[i] = renderItem(source[key], key, i, cached && cached[i]); + } + } + } else { + ret = []; + } + if (cache) { + cache[index] = ret; + } + return ret; +} +function createSlots(slots, dynamicSlots) { + for (let i = 0; i < dynamicSlots.length; i++) { + const slot = dynamicSlots[i]; + if (isArray(slot)) { + for (let j = 0; j < slot.length; j++) { + slots[slot[j].name] = slot[j].fn; + } + } else if (slot) { + slots[slot.name] = slot.key ? (...args) => { + const res = slot.fn(...args); + if (res) res.key = slot.key; + return res; + } : slot.fn; + } + } + return slots; +} +function renderSlot(slots, name, props = {}, fallback, noSlotted) { + if (currentRenderingInstance.ce || currentRenderingInstance.parent && isAsyncWrapper(currentRenderingInstance.parent) && currentRenderingInstance.parent.ce) { + const hasProps = Object.keys(props).length > 0; + if (name !== "default") props.name = name; + return openBlock(), createBlock( + Fragment, + null, + [createVNode("slot", props, fallback && fallback())], + hasProps ? -2 : 64 + ); + } + let slot = slots[name]; + if (slot && slot.length > 1) { + warn$1( + `SSR-optimized slot function detected in a non-SSR-optimized render function. You need to mark this component with $dynamic-slots in the parent template.` + ); + slot = () => []; + } + if (slot && slot._c) { + slot._d = false; + } + openBlock(); + const validSlotContent = slot && ensureValidVNode(slot(props)); + const slotKey = props.key || // slot content array of a dynamic conditional slot may have a branch + // key attached in the `createSlots` helper, respect that + validSlotContent && validSlotContent.key; + const rendered = createBlock( + Fragment, + { + key: (slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) + // #7256 force differentiate fallback content from actual content + (!validSlotContent && fallback ? "_fb" : "") + }, + validSlotContent || (fallback ? fallback() : []), + validSlotContent && slots._ === 1 ? 64 : -2 + ); + if (!noSlotted && rendered.scopeId) { + rendered.slotScopeIds = [rendered.scopeId + "-s"]; + } + if (slot && slot._c) { + slot._d = true; + } + return rendered; +} +function ensureValidVNode(vnodes) { + return vnodes.some((child) => { + if (!isVNode(child)) return true; + if (child.type === Comment) return false; + if (child.type === Fragment && !ensureValidVNode(child.children)) + return false; + return true; + }) ? vnodes : null; +} +function toHandlers(obj, preserveCaseIfNecessary) { + const ret = {}; + if (!isObject(obj)) { + warn$1(`v-on with no argument expects an object value.`); + return ret; + } + for (const key in obj) { + ret[preserveCaseIfNecessary && /[A-Z]/.test(key) ? `on:${key}` : toHandlerKey(key)] = obj[key]; + } + return ret; +} +var getPublicInstance = (i) => { + if (!i) return null; + if (isStatefulComponent(i)) return getComponentPublicInstance(i); + return getPublicInstance(i.parent); +}; +var publicPropertiesMap = ( + // Move PURE marker to new line to workaround compiler discarding it + // due to type annotation + extend(/* @__PURE__ */ Object.create(null), { + $: (i) => i, + $el: (i) => i.vnode.el, + $data: (i) => i.data, + $props: (i) => true ? shallowReadonly(i.props) : i.props, + $attrs: (i) => true ? shallowReadonly(i.attrs) : i.attrs, + $slots: (i) => true ? shallowReadonly(i.slots) : i.slots, + $refs: (i) => true ? shallowReadonly(i.refs) : i.refs, + $parent: (i) => getPublicInstance(i.parent), + $root: (i) => getPublicInstance(i.root), + $host: (i) => i.ce, + $emit: (i) => i.emit, + $options: (i) => __VUE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type, + $forceUpdate: (i) => i.f || (i.f = () => { + queueJob(i.update); + }), + $nextTick: (i) => i.n || (i.n = nextTick.bind(i.proxy)), + $watch: (i) => __VUE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP + }) +); +var isReservedPrefix = (key) => key === "_" || key === "$"; +var hasSetupBinding = (state, key) => state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key); +var PublicInstanceProxyHandlers = { + get({ _: instance }, key) { + if (key === "__v_skip") { + return true; + } + const { ctx, setupState, data, props, accessCache, type, appContext } = instance; + if (key === "__isVue") { + return true; + } + if (key[0] !== "$") { + const n = accessCache[key]; + if (n !== void 0) { + switch (n) { + case 1: + return setupState[key]; + case 2: + return data[key]; + case 4: + return ctx[key]; + case 3: + return props[key]; + } + } else if (hasSetupBinding(setupState, key)) { + accessCache[key] = 1; + return setupState[key]; + } else if (__VUE_OPTIONS_API__ && data !== EMPTY_OBJ && hasOwn(data, key)) { + accessCache[key] = 2; + return data[key]; + } else if (hasOwn(props, key)) { + accessCache[key] = 3; + return props[key]; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4; + return ctx[key]; + } else if (!__VUE_OPTIONS_API__ || shouldCacheAccess) { + accessCache[key] = 0; + } + } + const publicGetter = publicPropertiesMap[key]; + let cssModule, globalProperties; + if (publicGetter) { + if (key === "$attrs") { + track(instance.attrs, "get", ""); + markAttrsAccessed(); + } else if (key === "$slots") { + track(instance, "get", key); + } + return publicGetter(instance); + } else if ( + // css module (injected by vue-loader) + (cssModule = type.__cssModules) && (cssModule = cssModule[key]) + ) { + return cssModule; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4; + return ctx[key]; + } else if ( + // global properties + globalProperties = appContext.config.globalProperties, hasOwn(globalProperties, key) + ) { + { + return globalProperties[key]; + } + } else if (currentRenderingInstance && (!isString(key) || // #1091 avoid internal isRef/isVNode checks on component instance leading + // to infinite warning loop + key.indexOf("__v") !== 0)) { + if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) { + warn$1( + `Property ${JSON.stringify( + key + )} must be accessed via $data because it starts with a reserved character ("$" or "_") and is not proxied on the render context.` + ); + } else if (instance === currentRenderingInstance) { + warn$1( + `Property ${JSON.stringify(key)} was accessed during render but is not defined on instance.` + ); + } + } + }, + set({ _: instance }, key, value) { + const { data, setupState, ctx } = instance; + if (hasSetupBinding(setupState, key)) { + setupState[key] = value; + return true; + } else if (setupState.__isScriptSetup && hasOwn(setupState, key)) { + warn$1(`Cannot mutate + + + + diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 00000000..68dde8ed --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,19 @@ +// https://vitepress.dev/guide/custom-theme +import { h } from 'vue' +import type { Theme } from 'vitepress' +import DefaultTheme from 'vitepress/theme' +import './style.css' +import NotFound from './NotFound.vue' + +export default { + extends: DefaultTheme, + Layout: () => { + return h(DefaultTheme.Layout, null, { + // https://vitepress.dev/guide/extending-default-theme#layout-slots + "not-found": () => h(NotFound) + }) + }, + enhanceApp({ app, router, siteData }) { + // ... + } +} satisfies Theme diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css new file mode 100644 index 00000000..1e9cef00 --- /dev/null +++ b/docs/.vitepress/theme/style.css @@ -0,0 +1,159 @@ +/** + * Customize default theme styling by overriding CSS variables: + * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css + */ + +/** + * Colors + * + * Each colors have exact same color scale system with 3 levels of solid + * colors with different brightness, and 1 soft color. + * + * - `XXX-1`: The most solid color used mainly for colored text. It must + * satisfy the contrast ratio against when used on top of `XXX-soft`. + * + * - `XXX-2`: The color used mainly for hover state of the button. + * + * - `XXX-3`: The color for solid background, such as bg color of the button. + * It must satisfy the contrast ratio with pure white (#ffffff) text on + * top of it. + * + * - `XXX-soft`: The color used for subtle background such as custom container + * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors + * on top of it. + * + * The soft color must be semi transparent alpha channel. This is crucial + * because it allows adding multiple "soft" colors on top of each other + * to create an accent, such as when having inline code block inside + * custom containers. + * + * - `default`: The color used purely for subtle indication without any + * special meanings attached to it such as bg color for menu hover state. + * + * - `brand`: Used for primary brand colors, such as link text, button with + * brand theme, etc. + * + * - `tip`: Used to indicate useful information. The default theme uses the + * brand color for this by default. + * + * - `warning`: Used to indicate warning to the users. Used in custom + * container, badges, etc. + * + * - `danger`: Used to show error, or dangerous message to the users. Used + * in custom container, badges, etc. + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-default-1: var(--vp-c-gray-1); + --vp-c-default-2: var(--vp-c-gray-2); + --vp-c-default-3: var(--vp-c-gray-3); + --vp-c-default-soft: var(--vp-c-gray-soft); + /* GFramework Brand Colors */ + --vp-c-brand-1: #1e40af; /* 深蓝:文字 / active */ + --vp-c-brand-2: #2563eb; /* 蓝:hover */ + --vp-c-brand-3: #3b82f6; /* 主蓝:按钮 / 主色 */ + --vp-c-brand-soft: rgba(59, 130, 246, 0.14); + /*--vp-c-brand-1: var(--vp-c-indigo-1);*/ + /*--vp-c-brand-2: var(--vp-c-indigo-2);*/ + /*--vp-c-brand-3: var(--vp-c-indigo-3);*/ + /*--vp-c-brand-soft: var(--vp-c-indigo-soft);*/ + + --vp-c-tip-1: var(--vp-c-brand-1); + --vp-c-tip-2: var(--vp-c-brand-2); + --vp-c-tip-3: var(--vp-c-brand-3); + --vp-c-tip-soft: var(--vp-c-brand-soft); + + --vp-c-warning-1: var(--vp-c-yellow-1); + --vp-c-warning-2: var(--vp-c-yellow-2); + --vp-c-warning-3: var(--vp-c-yellow-3); + --vp-c-warning-soft: var(--vp-c-yellow-soft); + + --vp-c-danger-1: var(--vp-c-red-1); + --vp-c-danger-2: var(--vp-c-red-2); + --vp-c-danger-3: var(--vp-c-red-3); + --vp-c-danger-soft: var(--vp-c-red-soft); +} + +/** + * Component: Button + * -------------------------------------------------------------------------- */ + +:root { + --vp-button-brand-border: transparent; + --vp-button-brand-text: var(--vp-c-white); + --vp-button-brand-bg: var(--vp-c-brand-3); + --vp-button-brand-hover-border: transparent; + --vp-button-brand-hover-text: var(--vp-c-white); + --vp-button-brand-hover-bg: var(--vp-c-brand-2); + --vp-button-brand-active-border: transparent; + --vp-button-brand-active-text: var(--vp-c-white); + --vp-button-brand-active-bg: var(--vp-c-brand-1); +} + +/** + * Component: Home + * -------------------------------------------------------------------------- */ + +:root { + --vp-home-hero-name-color: transparent; + /*--vp-home-hero-name-background: -webkit-linear-gradient(*/ + /* 120deg,*/ + /* #bd34fe 30%,*/ + /* #41d1ff*/ + /*);*/ + --vp-home-hero-name-background: linear-gradient( + 120deg, + #22c55e 0%, + #3b82f6 45%, + #8b5cf6 100% + ); + + /*--vp-home-hero-image-background-image: linear-gradient(*/ + /* -45deg,*/ + /* #bd34fe 50%,*/ + /* #47caff 50%*/ + /*);*/ + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + rgba(34, 197, 94, 0.6), + rgba(59, 130, 246, 0.6), + rgba(139, 92, 246, 0.6) + ); + + --vp-home-hero-image-filter: blur(44px); +} + +@media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + +@media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(68px); + } +} + +/** + * Component: Custom Block + * -------------------------------------------------------------------------- */ + +:root { + --vp-custom-block-tip-border: transparent; + --vp-custom-block-tip-text: var(--vp-c-text-1); + --vp-custom-block-tip-bg: var(--vp-c-brand-soft); + --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); + + + --vp-button-brand-bg: linear-gradient( + 135deg, + #3b82f6, + #2563eb + ); +} +.VPFeature:hover { + box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.25), + 0 8px 32px rgba(59, 130, 246, 0.25); +} + diff --git a/docs/api-reference/core-api.md b/docs/api-reference/core-api.md deleted file mode 100644 index 44c4b55c..00000000 --- a/docs/api-reference/core-api.md +++ /dev/null @@ -1,1097 +0,0 @@ -# GFramework.Core API 参考 - -> GFramework.Core 模块的完整 API 参考文档,包含所有核心类、接口和方法的详细说明。 - -## 📋 目录 - -- [Architecture 核心架构](#architecture-核心架构) -- [Models 数据模型](#models-数据模型) -- [Systems 业务系统](#systems-业务系统) -- [Commands 命令模式](#commands-命令模式) -- [Queries 查询模式](#queries-查询模式) -- [Events 事件系统](#events-事件系统) -- [Properties 属性绑定](#properties-属性绑定) -- [IoC 容器](#ioc-容器) -- [Utilities 工具类](#utilities-工具类) -- [Extensions 扩展方法](#extensions-扩展方法) -- [Rules 规则接口](#rules-规则接口) - -## Architecture 核心架构 - -### IArchitecture - -架构接口,定义了框架的核心功能契约。 - -#### 方法签名 - -```csharp -// 组件注册 -void RegisterSystem(TSystem system) where TSystem : ISystem; -void RegisterModel(TModel model) where TModel : IModel; -void RegisterUtility(TUtility utility) where TUtility : IUtility; - -// 组件获取 -T GetModel() where T : class, IModel; -T GetSystem() where T : class, ISystem; -T GetUtility() where T : class, IUtility; - -// 命令处理 -void SendCommand(ICommand command); -TResult SendCommand(ICommand command); - -// 查询处理 -TResult SendQuery(IQuery query); - -// 事件管理 -void SendEvent(T e) where T : new(); -void SendEvent(T e); -IUnRegister RegisterEvent(Action onEvent) where T : new(); -void UnRegisterEvent(Action onEvent) where T : new(); -``` - -#### 使用示例 - -```csharp -// 注册组件 -architecture.RegisterModel(new PlayerModel()); -architecture.RegisterSystem(new CombatSystem()); -architecture.RegisterUtility(new StorageUtility()); - -// 获取组件 -var playerModel = architecture.GetModel(); -var combatSystem = architecture.GetSystem(); -var storageUtility = architecture.GetUtility(); - -// 发送命令 -architecture.SendCommand(new AttackCommand { TargetId = "enemy1" }); - -// 发送查询 -var canAttack = architecture.SendQuery(new CanAttackQuery { PlayerId = "player1" }); - -// 发送事件 -architecture.SendEvent(new PlayerDiedEvent { PlayerId = "player1" }); - -// 注册事件监听 -var unregister = architecture.RegisterEvent(OnPlayerDied); -``` - -### Architecture - -抽象架构基类,实现了 IArchitecture 接口。 - -#### 构造函数 - -```csharp -public abstract class Architecture( - IArchitectureConfiguration? configuration = null, - IEnvironment? environment = null, - IArchitectureServices? services = null, - IArchitectureContext? context = null -) -``` - -#### 主要属性 - -```csharp -public IArchitectureContext Context { get; } -public ArchitecturePhase Phase { get; } -public bool IsInitialized { get; } -public bool IsDestroyed { get; } -``` - -#### 主要方法 - -```csharp -// 初始化 -public void Initialize() -public async Task InitializeAsync() - -// 销毁 -public void Destroy() - -// 模块安装 -public void InstallModule(IArchitectureModule module) -public void InstallModule() where T : IArchitectureModule, new(); -``` - -#### 架构阶段 - -```csharp -public enum ArchitecturePhase -{ - None = 0, - BeforeUtilityInit = 1, - AfterUtilityInit = 2, - BeforeModelInit = 3, - AfterModelInit = 4, - BeforeSystemInit = 5, - AfterSystemInit = 6, - Ready = 7, - FailedInitialization = 8, - Destroying = 9, - Destroyed = 10 -} -``` - -#### 使用示例 - -```csharp -public class GameArchitecture : Architecture -{ - protected override void Init() - { - RegisterModel(new PlayerModel()); - RegisterSystem(new CombatSystem()); - RegisterUtility(new StorageUtility()); - } - - protected override void InstallModules() - { - InstallModule(new AudioModule()); - InstallModule(new SaveModule()); - } - - protected override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - switch (phase) - { - case ArchitecturePhase.Ready: - GD.Print("Architecture is ready"); - break; - case ArchitecturePhase.Destroying: - GD.Print("Architecture is destroying"); - break; - } - } -} -``` - -### IArchitectureModule - -架构模块接口。 - -#### 方法签名 - -```csharp -void Install(IArchitecture architecture); -``` - -#### 使用示例 - -```csharp -public class AudioModule : IArchitectureModule -{ - public void Install(IArchitecture architecture) - { - architecture.RegisterSystem(new AudioSystem()); - architecture.RegisterUtility(new AudioUtility()); - } -} -``` - -## Models 数据模型 - -### IModel - -模型接口,定义了模型的基本行为。 - -#### 继承的接口 - -```csharp -IContextAware -ILogAware -IAsyncInitializable -IArchitecturePhaseAware -``` - -#### 方法签名 - -```csharp -void Init(); -void OnArchitecturePhase(ArchitecturePhase phase); -``` - -### AbstractModel - -抽象模型基类,实现了 IModel 接口。 - -#### 使用示例 - -```csharp -public class PlayerModel : AbstractModel -{ - public BindableProperty Health { get; } = new(100); - public BindableProperty MaxHealth { get; } = new(100); - public BindableProperty Position { get; } = new(Vector2.Zero); - public BindableProperty State { get; } = new(PlayerState.Idle); - - protected override void OnInit() - { - Health.Register(OnHealthChanged); - State.Register(OnStateChanged); - } - - protected override void OnArchitecturePhase(ArchitecturePhase phase) - { - if (phase == ArchitecturePhase.Ready) - { - // 架构准备就绪 - SendEvent(new PlayerModelReadyEvent()); - } - } - - private void OnHealthChanged(int newHealth) - { - if (newHealth <= 0) - { - State.Value = PlayerState.Dead; - SendEvent(new PlayerDiedEvent()); - } - } - - private void OnStateChanged(PlayerState newState) - { - SendEvent(new PlayerStateChangedEvent { NewState = newState }); - } -} - -public enum PlayerState -{ - Idle, - Moving, - Attacking, - Dead -} -``` - -## Systems 业务系统 - -### ISystem - -系统接口,定义了系统的基本行为。 - -#### 继承的接口 - -```csharp -IContextAware -ILogAware -IAsyncInitializable -IArchitecturePhaseAware -``` - -#### 方法签名 - -```csharp -void Init(); -void OnArchitecturePhase(ArchitecturePhase phase); -``` - -### AbstractSystem - -抽象系统基类,实现了 ISystem 接口。 - -#### 使用示例 - -```csharp -public class CombatSystem : AbstractSystem -{ - private PlayerModel _playerModel; - private EnemyModel _enemyModel; - - protected override void OnInit() - { - _playerModel = GetModel(); - _enemyModel = GetModel(); - - // 注册事件监听 - this.RegisterEvent(OnAttackInput); - this.RegisterEvent(OnEnemyDeath); - } - - protected override void OnArchitecturePhase(ArchitecturePhase phase) - { - if (phase == ArchitecturePhase.Ready) - { - // 系统准备就绪 - Logger.Info("CombatSystem is ready"); - } - } - - private void OnAttackInput(AttackInputEvent e) - { - if (_playerModel.State.Value == PlayerState.Attacking) - return; - - var enemy = FindEnemyInRange(); - if (enemy != null) - { - PerformAttack(_playerModel, enemy); - } - } - - private void OnEnemyDeath(EnemyDeathEvent e) - { - Logger.Info($"Enemy {e.EnemyId} died"); - - // 更新玩家分数 - _playerModel.Score.Value += 100; - - // 发送事件通知其他系统 - SendEvent(new EnemyDefeatedEvent { EnemyId = e.EnemyId }); - } - - private Enemy FindEnemyInRange() - { - // 查找范围内的敌人逻辑 - return null; - } - - private void PerformAttack(PlayerModel player, Enemy enemy) - { - var damage = CalculateDamage(player, enemy); - enemy.Health.Value -= damage; - - SendEvent(new DamageDealtEvent - { - AttackerId = player.Id, - TargetId = enemy.Id, - Damage = damage - }); - } - - private int CalculateDamage(PlayerModel player, Enemy enemy) - { - // 伤害计算逻辑 - return player.AttackPower.Value; - } -} -``` - -## Commands 命令模式 - -### ICommand - -命令接口,定义了命令的基本行为。 - -#### 方法签名 - -```csharp -void Execute(); -``` - -### ICommand - -带返回值的命令接口。 - -#### 方法签名 - -```csharp -TResult Execute(); -``` - -### AbstractCommand - -抽象命令基类,实现了 ICommand 接口。 - -#### 可用的方法 - -```csharp -// 获取模型 -T GetModel() where T : class, IModel; - -// 获取系统 -T GetSystem() where T : class, ISystem; - -// 获取工具 -T GetUtility() where T : class, IUtility; - -// 发送事件 -void SendEvent(T e) where T : new(); -void SendEvent(T e); - -// 发送命令 -void SendCommand(ICommand command); -TResult SendCommand(ICommand command); - -// 发送查询 -TResult SendQuery(IQuery query); -``` - -#### 使用示例 - -```csharp -public class AttackCommand : AbstractCommand -{ - public string AttackerId { get; set; } - public string TargetId { get; set; } - public int Damage { get; set; } - - protected override void OnExecute() - { - var attacker = GetModel(AttackerId); - var target = GetModel(TargetId); - - if (!CanAttack(attacker, target)) - return; - - PerformAttack(attacker, target, Damage); - - SendEvent(new AttackCompletedEvent - { - AttackerId = AttackerId, - TargetId = TargetId, - Damage = Damage - }); - } - - private bool CanAttack(PlayerModel attacker, EnemyModel target) - { - return attacker.State.Value == PlayerState.Idle && - target.Health.Value > 0 && - Vector2.Distance(attacker.Position.Value, target.Position.Value) <= attacker.AttackRange.Value; - } - - private void PerformAttack(PlayerModel attacker, EnemyModel target, int damage) - { - target.Health.Value -= damage; - - SendEvent(new DamageDealtEvent - { - AttackerId = attacker.Id, - TargetId = target.Id, - Damage = damage - }); - } -} - -public class GetPlayerScoreQuery : AbstractQuery -{ - public string PlayerId { get; set; } - - protected override int OnDo() - { - var playerModel = GetModel(PlayerId); - return playerModel.Score.Value; - } -} - -// 使用示例 -var attackCommand = new AttackCommand -{ - AttackerId = "player1", - TargetId = "enemy1", - Damage = 50 -}; - -Context.SendCommand(attackCommand); - -var scoreQuery = new GetPlayerScoreQuery { PlayerId = "player1" }; -var score = Context.SendQuery(scoreQuery); -``` - -## Queries 查询模式 - -### IQuery - -查询接口,定义了查询的基本行为。 - -#### 方法签名 - -```csharp -TResult OnDo(); -``` - -### AbstractQuery - -抽象查询基类,实现了 IQuery 接口。 - -#### 可用的方法 - -与 AbstractCommand 相同,提供: - -- GetModel\() -- GetSystem\() -- GetUtility\() -- SendEvent\() -- SendCommand() -- SendQuery() - -#### 使用示例 - -```csharp -public class CanAttackQuery : AbstractQuery -{ - public string PlayerId { get; set; } - public string TargetId { get; set; } - - protected override bool OnDo() - { - var player = GetModel(PlayerId); - var target = GetModel(TargetId); - - if (player.State.Value != PlayerState.Idle) - return false; - - if (player.Health.Value <= 0) - return false; - - if (target.Health.Value <= 0) - return false; - - var distance = Vector2.Distance( - player.Position.Value, - target.Position.Value - ); - - return distance <= player.AttackRange.Value; - } -} - -public class GetInventoryItemsQuery : AbstractQuery> -{ - public string PlayerId { get; set; } - public string ItemType { get; set; } - - protected override List OnDo() - { - var inventorySystem = GetSystem(); - return inventorySystem.GetPlayerItems(PlayerId, ItemType); - } -} - -public class GetGameStatisticsQuery : AbstractQuery -{ - protected override GameStatistics OnDo() - { - var playerModel = GetModel(); - var gameModel = GetModel(); - - return new GameStatistics - { - PlayerLevel = playerModel.Level.Value, - PlayerScore = playerModel.Score.Value, - PlayerHealth = playerModel.Health.Value, - PlayTime = gameModel.PlayTime.Value, - EnemiesDefeated = gameModel.EnemiesDefeated.Value - }; - } -} - -// 使用示例 -var canAttackQuery = new CanAttackQuery { PlayerId = "player1", TargetId = "enemy1" }; -var canAttack = Context.SendQuery(canAttackQuery); - -var inventoryQuery = new GetInventoryItemsQuery -{ - PlayerId = "player1", - ItemType = "weapon" -}; -var weapons = Context.SendQuery(inventoryQuery); - -var statsQuery = new GetGameStatisticsQuery(); -var stats = Context.SendQuery(statsQuery); -``` - -## Events 事件系统 - -### IEvent - -事件标记接口,用于标识事件类型。 - -```csharp -public interface IEvent -{ - // 事件类型标识 -} -``` - -### IUnRegister - -注销接口,用于取消事件注册。 - -#### 方法签名 - -```csharp -void UnRegister(); -``` - -### IUnRegisterList - -注销列表接口,用于批量管理注销对象。 - -#### 属性 - -```csharp -IList UnregisterList { get; } -``` - -#### 方法签名 - -```csharp -void Add(IUnRegister unRegister); -void UnRegisterAll(); -``` - -### EasyEvent - -无参事件类。 - -#### 构造函数 - -```csharp -public EasyEvent() -``` - -#### 方法 - -```csharp -IUnRegister Register(Action onEvent); -void Trigger(); -``` - -#### 使用示例 - -```csharp -// 创建事件 -var buttonClicked = new EasyEvent(); - -// 注册监听 -var unregister = buttonClicked.Register(() => { - GD.Print("Button clicked!"); -}); - -// 触发事件 -buttonClicked.Trigger(); - -// 注销监听 -unregister.UnRegister(); -``` - -### Event - -单参数泛型事件类。 - -#### 构造函数 - -```csharp -public Event() -``` - -#### 方法 - -```csharp -IUnRegister Register(Action onEvent); -void Trigger(T data); -``` - -#### 使用示例 - -```csharp -// 创建事件 -var scoreChanged = new Event(); - -// 注册监听 -var unregister = scoreChanged.Register(score => { - GD.Print($"Score changed to: {score}"); -}); - -// 触发事件 -scoreChanged.Trigger(100); - -// 注销监听 -unregister.UnRegister(); -``` - -### EventBus - -事件总线类,提供基于类型的事件发送和注册。 - -#### 方法 - -```csharp -IUnRegister Register(Action onEvent) where T : new(); -void Send(T e) where T : new(); -void Send() where T : new(); -``` - -#### 使用示例 - -```csharp -// 创建事件总线 -var eventBus = new EventBus(); - -// 注册事件 -eventBus.Register(e => { - GD.Print($"Player died at position: {e.Position}"); -}); - -// 发送事件 -eventBus.Send(new PlayerDiedEvent -{ - Position = new Vector3(10, 0, 5) -}); -``` - -## Properties 属性绑定 - -### BindableProperty - -可绑定属性类,支持属性变化监听。 - -#### 构造函数 - -```csharp -public BindableProperty(T initialValue = default) -``` - -#### 属性 - -```csharp -public T Value { get; set; } -public T PreviousValue { get; } -``` - -#### 方法 - -```csharp -IUnRegister Register(Action onValueChanged); -IUnRegister RegisterWithInitValue(Action onValueChanged); -void SetValueWithoutNotify(T value); -``` - -#### 使用示例 - -```csharp -public class PlayerModel : AbstractModel -{ - public BindableProperty Health { get; } = new(100); - public BindableProperty Position { get; } = new(Vector2.Zero); - - protected override void OnInit() - { - // 注册健康值变化监听 - Health.Register(OnHealthChanged); - - // 注册位置变化监听(包含初始值) - Position.RegisterWithInitValue(OnPositionChanged); - } - - private void OnHealthChanged(int newHealth) - { - if (newHealth <= 0) - { - SendEvent(new PlayerDiedEvent()); - } - } - - private void OnPositionChanged(Vector2 newPosition) - { - // 处理位置变化 - UpdatePlayerPosition(newPosition); - } -} - -// 外部使用 -var playerModel = new PlayerModel(); - -// 监听属性变化 -var healthUnregister = playerModel.Health.Register(health => { - GD.Print($"Player health: {health}"); -}); - -// 设置属性值 -playerModel.Health.Value = 80; // 会触发监听器 -playerModel.Health.SetValueWithoutNotify(90); // 不会触发监听器 - -// 注销监听 -healthUnregister.UnRegister(); -``` - -## IoC 容器 - -### IBelongToArchitecture - -属于架构的接口标记。 - -### IContextAware - -上下文感知接口。 - -#### 属性 - -```csharp -IContextAware.Context Context { get; } -void SetContext(IContextAware.Context context); -``` - -### ILogAware - -日志感知接口。 - -#### 属性 - -```csharp -ILogger Logger { get; } -``` - -### ContextAwareBase - -上下文感知基类,实现了 IContextAware 接口。 - -#### 属性 - -```csharp -public IContextAware.Context Context { get; } -``` - -#### 使用示例 - -```csharp -public class MyService : ContextAwareBase -{ - public void DoWork() - { - var playerModel = Context.GetModel(); - Logger.Info("Doing work with player model"); - } -} -``` - -## Utilities 工具类 - -### IUtility - -工具接口,标记工具类。 - -#### 方法签名 - -```csharp -void Init(); -``` - -### AbstractUtility - -抽象工具基类。 - -#### 可用的方法 - -与 AbstractCommand 相同,提供: - -- GetModel\() -- GetSystem\() -- GetUtility\() -- SendEvent\() -- SendCommand() -- SendQuery() - -#### 使用示例 - -```csharp -public class MathUtility : AbstractUtility -{ - public float Distance(Vector2 a, Vector2 b) - { - return Vector2.Distance(a, b); - } - - public float Lerp(float a, float b, float t) - { - return a + (b - a) * t; - } - - public Vector2 Normalize(Vector2 vector) - { - if (vector == Vector2.Zero) - return Vector2.Zero; - - return vector.Normalized(); - } -} - -public class StorageUtility : AbstractUtility -{ - private readonly IStorage _storage; - - public StorageUtility(IStorage storage) - { - _storage = storage; - } - - protected override void OnInit() - { - Logger.Info("Storage utility initialized"); - } - - public async Task SaveAsync(string key, T data) - { - await _storage.WriteAsync(key, data); - Logger.Info($"Saved data to key: {key}"); - } - - public async Task LoadAsync(string key, T defaultValue = default) - { - var data = await _storage.ReadAsync(key, defaultValue); - Logger.Info($"Loaded data from key: {key}"); - return data; - } -} -``` - -## Extensions 扩展方法 - -### 组件获取扩展 - -```csharp -// 为 IArchitectureContext 提供扩展方法 -public static class ArchitectureContextExtensions -{ - public static T GetModel(this IContextAware.Context context) where T : class, IModel; - public static T GetSystem(this IContextAware.Context context) where T : class, ISystem; - public static T GetUtility(this IContextAware.Context context) where T : class, IUtility; -} -``` - -### 事件扩展 - -```csharp -// 为事件注册提供扩展方法 -public static class EventExtensions -{ - public static IUnRegister UnRegisterWhenNodeExitTree( - this IUnRegister unRegister, - Node node) where T : new(); - - public static IUnRegister AddToUnregisterList( - this IUnRegister unRegister, - IUnRegisterList unregisterList); -} -``` - -#### 使用示例 - -```csharp -// 使用扩展方法 -var playerModel = Context.GetModel(); -var combatSystem = Context.GetSystem(); -var storageUtility = Context.GetUtility(); - -// 使用事件扩展 -var unregister = this.RegisterEvent(OnPlayerDied) - .UnRegisterWhenNodeExitTree(this); - -var unregisterList = new UnRegisterList(); -this.RegisterEvent(OnGameEvent) - .AddToUnregisterList(unregisterList); -``` - -## Rules 规则接口 - -### IRule - -规则接口标记。 - -### ICanGetModel - -可获取模型的规则接口。 - -```csharp -T GetModel() where T : class, IModel; -``` - -### ICanGetSystem - -可获取系统的规则接口。 - -```csharp -T GetSystem() where T : class, ISystem; -``` - -### ICanGetUtility - -可获取工具的规则接口。 - -```csharp -T GetUtility() where T : class, IUtility; -``` - -### ICanSendCommand - -可发送命令的规则接口。 - -```csharp -void SendCommand(ICommand command); -TResult SendCommand(ICommand command); -``` - -### ICanSendQuery - -可发送查询的规则接口。 - -```csharp -TResult SendQuery(IQuery query); -``` - -### ICanRegisterEvent - -可注册事件的规则接口。 - -```csharp -IUnRegister RegisterEvent(Action onEvent) where T : new(); -``` - -### IController - -控制器接口,组合了多个规则接口。 - -```csharp -public interface IController : - ICanGetModel, - ICanGetSystem, - ICanGetUtility, - ICanSendCommand, - ICanSendQuery, - ICanRegisterEvent -{ -} -``` - -## 错误处理 - -### GFrameworkException - -框架基础异常类。 - -```csharp -public class GFrameworkException : Exception -{ - public GFrameworkException(string message) : base(message) { } - public GFrameworkException(string message, Exception innerException) : base(message, innerException) { } -} -``` - -### ArchitectureException - -架构相关异常。 - -```csharp -public class ArchitectureException : GFrameworkException -{ - public ArchitectureException(string message) : base(message) { } -} -``` - -### ComponentException - -组件相关异常。 - -```csharp -public class ComponentException : GFrameworkException -{ - public ComponentException(string message) : base(message) { } -} -``` - ---- - -**文档版本**: 1.0.0 -**更新日期**: 2026-01-12 \ No newline at end of file diff --git a/docs/api-reference/game-api.md b/docs/api-reference/game-api.md deleted file mode 100644 index d43d95c8..00000000 --- a/docs/api-reference/game-api.md +++ /dev/null @@ -1,834 +0,0 @@ -# GFramework.Game API 参考 - -> GFramework.Game 模块的完整 API 参考文档,包含游戏特定功能的详细说明。 - -## 📋 目录 - -- [架构模块](#架构模块) -- [资产管理系统](#资产管理系统) -- [存储系统](#存储系统) -- [序列化系统](#序列化系统) -- [数据模型](#数据模型) -- [工具类](#工具类) - -## 架构模块 - -### IArchitectureModule - -架构模块接口,定义了模块的基本行为。 - -#### 方法签名 - -```csharp -void Install(IArchitecture architecture); -void OnAttach(Architecture architecture); -void OnDetach(Architecture architecture); -void OnPhase(ArchitecturePhase phase, IArchitecture architecture); -``` - -#### 使用示例 - -```csharp -public class AudioModule : IArchitectureModule -{ - public void Install(IArchitecture architecture) - { - architecture.RegisterSystem(new AudioSystem()); - architecture.RegisterUtility(new AudioUtility()); - } - - public void OnAttach(Architecture architecture) - { - Logger.Info("Audio module attached to architecture"); - } - - public void OnDetach(Architecture architecture) - { - Logger.Info("Audio module detached from architecture"); - } - - public void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - switch (phase) - { - case ArchitecturePhase.Ready: - // 架构准备就绪,可以开始播放背景音乐 - PlayBackgroundMusic(); - break; - case ArchitecturePhase.Destroying: - // 架构正在销毁,清理音频资源 - CleanupAudioResources(); - break; - } - } -} -``` - -### AbstractModule - -抽象模块基类,实现了 IArchitectureModule 接口。 - -#### 构造函数 - -```csharp -public AbstractModule(); -``` - -#### 可用方法 - -```csharp -// 发送事件 -protected void SendEvent(T e) where T : new(); -protected void SendEvent(T e); - -// 获取模型 -protected T GetModel() where T : class, IModel; -protected T GetSystem() where T : class, ISystem; -protected T GetUtility() where T : class, IUtility; - -// 发送命令 -protected void SendCommand(ICommand command); -protected TResult SendCommand(ICommand command); - -// 发送查询 -protected TResult SendQuery(IQuery query); -``` - -#### 使用示例 - -```csharp -public class SaveModule : AbstractModule -{ - private Timer _autoSaveTimer; - - public override void Install(IArchitecture architecture) - { - architecture.RegisterSystem(new SaveSystem()); - architecture.RegisterUtility(new SaveUtility()); - } - - public override void OnAttach(Architecture architecture) - { - // 创建自动保存计时器 - _autoSaveTimer = new Timer(); - _autoSaveTimer.WaitTime = 300; // 5分钟 - _autoSaveTimer.Timeout += OnAutoSave; - // 这里无法使用 AddChild,因为不是 Node - // 在具体的模块实现中处理 - } - - public override void OnDetach(Architecture architecture) - { - _autoSaveTimer?.Stop(); - _autoSaveTimer = null; - } - - public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - switch (phase) - { - case ArchitecturePhase.Ready: - Logger.Info("Save module ready"); - break; - case ArchitecturePhase.Destroying: - Logger.Info("Save module destroying"); - break; - } - } - - private void OnAutoSave() - { - var saveSystem = GetSystem(); - saveSystem.SaveAutoSave(); - } -} -``` - -## 资产管理系统 - -### IAssetCatalogUtility - -资产目录工具接口,定义了资产管理的基本行为。 - -#### 方法签名 - -```csharp -// 场景资产注册 -void RegisterSceneUnit(string key, string scenePath); -void RegisterScenePage(string key, string scenePath); -void RegisterAsset(string key, string path) where T : Resource; -void RegisterAsset(string key, Type type, string path); - -// 映射资产注册 -void RegisterSceneUnit(string key, AssetMapping mapping); -void RegisterScenePage(string key, AssetMapping mapping); -void RegisterAsset(string key, AssetMapping mapping); - -// 查询方法 -bool HasSceneUnit(string key); -bool HasScenePage(string key); -bool HasAsset(string key); -bool HasAsset(string key, Type type); - -// 获取方法 -T GetScene(string key) where T : PackedScene; -T GetScenePage(string key) where T : PackedScene; -T GetAsset(string key) where T : Resource; -T GetAsset(string key, Type type); - -// 元数据获取 -AssetCatalogMapping GetAssetMetadata(string key); -``` - -#### AssetCatalogMapping - -资产映射数据类。 - -#### 属性 - -```csharp -public string Key { get; set; } -public string Path { get; set; } -public Type Type { get; set; } -public Dictionary Metadata { get; set; } -public DateTime RegisteredAt { get; set; } -``` - -#### 使用示例 - -```csharp -public class GameAssetCatalog : AbstractAssetCatalogUtility -{ - public override void Initialize() - { - // 注册场景资产 - RegisterSceneUnit("Player", "res://scenes/Player.tscn"); - RegisterSceneUnit("Enemy", "res://scenes/Enemy.tscn"); - RegisterScenePage("MainMenu", "res://ui/MainMenu.tscn"); - RegisterScenePage("GameUI", "res://ui/GameUI.tscn"); - - // 注册通用资产 - RegisterAsset("PlayerTexture", "res://textures/player.png"); - RegisterAsset("EnemyTexture", "res://textures/enemy.png"); - RegisterAsset("ShootSound", "res://audio/shoot.wav"); - RegisterAsset("ExplosionSound", "res://audio/explosion.wav"); - - // 使用元数据注册 - var playerMapping = new AssetMapping - { - Key = "Player", - Path = "res://scenes/Player.tscn", - Type = typeof(PackedScene), - Metadata = new Dictionary - { - ["category"] = "character", - ["tags"] = new[] { "player", "hero", "controlled" }, - ["health"] = 100, - ["speed"] = 5.0f - } - }; - RegisterSceneUnit("Player", playerMapping); - } - - protected override bool ValidateAsset(string key, string path) - { - if (!FileAccess.FileExists(path)) - { - GD.PrintErr($"Asset file not found: {path}"); - return false; - } - - if (string.IsNullOrWhiteSpace(key)) - { - GD.PrintErr("Asset key cannot be empty"); - return false; - } - - return true; - } - - protected override void OnAssetLoaded(string key, object asset) - { - GD.Print($"Asset loaded: {key} of type {asset.GetType().Name}"); - - // 对特定资产进行额外处理 - if (key == "Player" && asset is PackedScene playerScene) - { - PreloadPlayerComponents(playerScene); - } - } - - private void PreloadPlayerComponents(PackedScene playerScene) - { - // 预加载玩家组件到内存 - playerScene.Instantiate(); // 实例化以加载子节点 - } - - // 获取资产 - public PackedScene GetPlayerScene() => GetScene("Player"); - public PackedScene GetEnemyScene() => GetScene("Enemy"); - public Texture2D GetPlayerTexture() => GetAsset("PlayerTexture"); - public AudioStream GetShootSound() => GetAsset("ShootSound"); -} -``` - -### AbstractResourceFactoryUtility - -抽象资源工厂工具基类。 - -#### 抽象方法 - -```csharp -protected abstract void RegisterFactories(); -protected abstract T CreateFactory(string path, Dictionary metadata = null) where T : class; -``` - -#### 使用示例 - -```csharp -public class GameResourceFactory : AbstractResourceFactoryUtility -{ - protected override void RegisterFactories() - { - RegisterFactory("res://data/players/{id}.json"); - RegisterFactory("res://data/weapons/{id}.json"); - RegisterFactory("res://data/levels/{id}.json"); - } - - public PlayerData CreatePlayer(string playerId) - { - var playerPath = $"res://data/players/{playerId}.json"; - return CreateFactory(playerPath); - } - - public WeaponConfig CreateWeapon(string weaponId) - { - var metadata = new Dictionary - { - ["weaponId"] = weaponId, - ["loadTime"] = DateTime.Now - }; - - return CreateFactory($"res://data/weapons/{weaponId}.json", metadata); - } - - public LevelData CreateLevel(string levelId) - { - return CreateFactory($"res://data/levels/{levelId}.json"); - } - - protected override PlayerData CreateFactory(string path, Dictionary metadata) - { - if (!FileAccess.FileExists(path)) - { - return new PlayerData(); // 返回默认数据 - } - - try - { - var json = FileAccess.Open(path, FileAccess.ModeFlags.Read).GetAsText(); - var data = JsonConvert.DeserializeObject(json); - - // 如果有元数据,可以用来修改数据 - if (metadata != null) - { - data.LastLoadedAt = metadata.GetValueOrDefault("loadTime", DateTime.Now); - data.LoadCount = metadata.GetValueOrDefault("loadCount", 0) + 1; - } - - return data; - } - catch (Exception ex) - { - GD.PrintErr($"Failed to load player data from {path}: {ex.Message}"); - return new PlayerData(); - } - } -} -``` - -## 存储系统 - -### IStorage - -存储接口,定义了数据持久化的基本行为。 - -#### 方法签名 - -```csharp -// 读写操作 -void Write(string key, T data); -T Read(string key, T defaultValue = default); -Task WriteAsync(string key, T data); -Task ReadAsync(string key, T defaultValue = default); - -// 存在检查 -bool Has(string key); -void Delete(string key); -void Clear(); -``` - -#### IScopedStorage - -作用域存储接口,支持命名空间隔离。 - -#### 构造函数 - -```csharp -public ScopedStorage(IStorage storage, string scope, string delimiter = "."); -``` - -#### 使用示例 - -```csharp -// 创建根存储 -var rootStorage = new FileStorage("user://data/"); - -// 创建分层存储 -var playerStorage = new ScopedStorage(rootStorage, "player"); -var saveStorage = new ScopedStorage(rootStorage, "saves"); -var settingsStorage = new ScopedStorage(rootStorage, "settings"); - -// 使用分层存储 -playerStorage.Write("profile", playerProfile); -playerStorage.Write("inventory", inventory); -playerStorage.Write("stats", playerStats); - -saveStorage.Write("slot_1", saveData); -saveStorage.Write("slot_2", saveData); - -settingsStorage.Write("graphics", graphicsSettings); -settingsStorage.Write("audio", audioSettings); - -// 读取数据 -var profile = playerStorage.Read("profile", new PlayerProfile()); -var inventory = playerStorage.Read("inventory", new Inventory()); - -// 使用完整路径 -playerStorage.Write("savegames/auto", autoSaveData); // 写入 player/savegames/auto.json -``` - -### FileStorage - -文件存储实现,基于 Godot 的 FileAccess。 - -#### 构造函数 - -```csharp -public FileStorage(string rootPath, bool createDirectoryIfNotExists = true); -``` - -#### 使用示例 - -```csharp -// 创建文件存储 -var storage = new FileStorage("user://saves"); - -// 基础用法 -storage.Write("player", playerData); -var loadedPlayer = storage.Read("player", new Player()); - -// 异步用法 -await storage.WriteAsync("player", playerData); -var loadedPlayer = await storage.ReadAsync("player"); - -// 检查存在性 -bool hasPlayerData = storage.Has("player"); -storage.Delete("old_save"); -storage.Clear(); -``` - -### JsonStorage - -JSON 存储实现,使用 Newtonsoft.Json 进行序列化。 - -#### 构造函数 - -```csharp -public JsonStorage(IFileStorage fileStorage, JsonSerializerSettings settings = null); -``` - -#### 使用示例 - -```csharp -var fileStorage = new FileStorage("user://saves"); -var settings = new JsonSerializerSettings -{ - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Populate -}; - -var storage = new JsonStorage(fileStorage, settings); - -// 自定义序列化 -storage.Write("player", playerData, customSerializer); - -// 压缩缩存储 -storage.Write("player", playerData, serializer: new JsonSerializer -{ - Formatting = Formatting.None -}); -``` - -### CachedStorage - -缓存存储实现,提高频繁访问的性能。 - -#### 构造函数 - -```csharp -public CachedStorage(IStorage innerStorage, TimeSpan cacheExpiry = default); -``` - -#### 使用示例 - -```csharp -var fileStorage = new FileStorage("user://saves"); -var cachedStorage = new CachedStorage(fileStorage, TimeSpan.FromMinutes(5)); - -// 第一次读取会从存储加载 -var player1 = cachedStorage.Read("player"); - -// 第二次读取会从缓存返回(如果未过期) -var player2 = cachedStorage.Read("player"); - -// 手动清除缓存 -cachedStorage.ClearCache(); - -// 检查缓存状态 -var playerInCache = cachedStorage.IsCached("player"); -``` - -## 序列化系统 - -### JsonSerializer - -JSON 序列化工具类,基于 Newtonsoft.Json。 - -#### 构造函数 - -```csharp -public JsonSerializer(JsonSerializerSettings settings = null); -``` - -#### 方法 - -```csharp -// 基础序列化 -public string Serialize(T data); -public void SerializeToFile(string path, T data); - -// 反序列化 -public T Deserialize(string json); -public T DeserializeFromFile(string path, T defaultValue = default); - -// 异步序列化 -public Task SerializeAsync(T data); -public Task DeserializeAsync(string json, T defaultValue = default); - -// 压缩序列化 -public string SerializeCompressed(T data); -public T DeserializeCompressed(string compressedJson); -``` - -#### 使用示例 - -```csharp -var serializer = new JsonSerializer(); - -// 基础序列化 -var json = serializer.Serialize(playerData); -var loadedPlayer = serializer.Deserialize(json); - -// 文件操作 -serializer.SerializeToFile("player.json", playerData); -var loadedFromFile = serializer.DeserializeFromFile("player.json"); - -// 异步操作 -var json = await serializer.SerializeAsync(playerData); -var loadedAsync = await serializer.DeserializeAsync(json); - -// 压缩操作 -var compressed = serializer.SerializeCompressed(playerData); -var decompressed = serializer.DeserializeCompressed(compressed); -``` - -### ISerializable - -可序列化接口。 - -#### 使用示例 - -```csharp -public class PlayerData : ISerializable -{ - public string PlayerId { get; set; } - public int Level { get; set; } - public int Score { get; set; } - public Vector2 Position { get; set; } - - [JsonProperty("last_saved_at")] - public DateTime LastSavedAt { get; set; } - - [JsonProperty("inventory_items")] - public List Inventory { get; set; } - - // 自定义序列化方法 - [OnSerializing] - public void OnSerializing() - { - // 序列化前的处理 - LastSavedAt = DateTime.Now; - } - - [OnDeserialized] - public void OnDeserialized() - { - // 反序列化后的处理 - if (LastSavedAt == default) - { - LastSavedAt = DateTime.Now; - } - } -} -``` - -## 数据模型 - -### SaveData - -存档数据类。 - -#### 属性 - -```csharp -public string Version { get; set; } = "1.0.0"; -public DateTime SavedAt { get; set; } -public Dictionary PlayerData { get; set; } = new(); -public Dictionary GameData { get; set; } = new(); -public Dictionary SystemData { get; set; } = new(); -public Dictionary ModuleData { get; set; } = new(); -public Dictionary SettingsData { get; set; } = new(); -``` - -### PlayerData - -玩家数据类。 - -#### 属性 - -```csharp -[JsonProperty("player_id")] -public string PlayerId { get; set; } - -[JsonProperty("player_name")] -public string PlayerName { get; set; } - -[JsonProperty("level")] -public int Level { get; set; } - -[JsonProperty("experience")] -public long Experience { get; set; } - -[JsonProperty("health")] -public int Health { get; set; } -public int MaxHealth { get; set; } - -[JsonProperty("position")] -public Vector3 Position { get; set; } - -[JsonProperty("inventory")] -public InventoryData Inventory { get; set; } - -[JsonProperty("skills")] -public List Skills { get; set; } -} -``` - -### GameData - -游戏数据类。 - -#### 属性 - -```csharp -[JsonProperty("current_level")] -public int CurrentLevel { get; set; } - -[JsonProperty("high_score")] -public int HighScore { get; set; } - -[JsonProperty("total_play_time")] -public float TotalPlayTime { get; set; } - -[JsonProperty("enemies_defeated")] -public int EnemiesDefeated { get; set; } - -[JsonProperty("achievements_unlocked")] -public List AchievementsUnlocked { get; set; } -``` - -### InventoryData - -背包数据类。 - -#### 属性 - -```csharp -[JsonProperty("slots")] -public List Slots { get; set; } = new(); - -[JsonProperty("equipment")] -public Dictionary Equipment { get; set; } = new(); -``` - -### InventorySlot - -背包槽位类。 - -#### 属性 - -```csharp -[JsonProperty("item_id")] -public string ItemId { get; set; } - -[JsonProperty("quantity")] -public int Quantity { get; set; } - -[JsonProperty("durability")] -public int Durability { get; set; } - -[JsonProperty("metadata")] -public Dictionary Metadata { get; set; } = new(); -``` - -## 工具类 - -### StorageUtility - -存储工具类,提供高级存储功能。 - -#### 构造函数 - -```csharp -public StorageUtility(IStorage storage, IVersionMigrationManager migrationManager = null); -``` - -#### 方法 - -```csharp -// 自动保存 -public void EnableAutoSave(IArchitecture architecture, float intervalMinutes = 5.0f); -public void DisableAutoSave(); - -// 存档管理 -public void CreateSave(int slotId, SaveData data); -public SaveData LoadSave(int slotId); -public List GetSaveSlots(); -public void DeleteSave(int slotId); - -// 数据迁移 -public void RegisterMigration(int fromVersion, int toVersion, Func migrator); -``` - -#### 使用示例 - -```csharp -[ContextAware] -[Log] -public partial class GameManager : Node, IController -{ - private StorageUtility _storageUtility; - - protected override void OnInit() - { - _storageUtility = Context.GetUtility(); - - // 注册数据迁移 - _storageUtility.RegisterMigration(1, 2, MigratePlayerDataV1ToV2); - _storageUtility.RegisterMigration(2, 3, MigratePlayerDataV2ToV3); - - // 启用自动保存 - _storageUtility.EnableAutoSave(Context, 5.0f); - - // 监听存档相关事件 - this.RegisterEvent(OnSaveRequest); - this.RegisterEvent(OnLoadRequest); - } - - private void OnSaveRequest(SaveRequestEvent e) - { - var saveData = CreateSaveData(); - _storageUtility.CreateSave(e.SlotId, saveData); - } - - private void OnLoadRequest(LoadRequestEvent e) - { - var saveData = _storageUtility.LoadSave(e.SlotId); - if (saveData != null) - { - RestoreGameData(saveData); - } - } - - private SaveData CreateSaveData() - { - var playerModel = Context.GetModel(); - var gameModel = Context.GetModel(); - - return new SaveData - { - Version = "1.0.0", - SavedAt = DateTime.Now, - PlayerData = new Dictionary - { - ["player"] = playerModel.GetData(), - ["statistics"] = playerModel.GetStatistics() - }, - GameData = new Dictionary - { - ["game"] = gameModel.GetData() - } - }; - } - - private PlayerData MigratePlayerDataV1ToV2(PlayerData v1Data) - { - return new PlayerData - { - PlayerId = v1Data.PlayerId, - PlayerName = v1Data.PlayerName, - Level = v1Data.Level, - Experience = v1Data.Experience, - Health = v1Data.Health, - MaxHealth = Math.Max(100, v1Data.MaxHealth), // V2 新增最大生命值 - Position = v1Data.Position, - Inventory = v1Data.Inventory, - Skills = v1Data.Skills // V1 的 Kills 字段在 V2 中重命名为 Skills - }; - } - - private PlayerData MigratePlayerDataV2ToV3(PlayerData v2Data) - { - return new PlayerData - { - PlayerId = v2Data.PlayerId, - PlayerName = Versioning.GetVersionString(v2Data.Version), - Level = v2Data.Level, - Experience = v2Data.Experience, - Health = v2Data.Health, - MaxHealth = v2Data.MaxHealth, - Position = v2Data.Position, - Inventory = v2Data.Inventory, - Skills = v2Data.Skills, - Achievements = v2Data.Achievements ?? new List() // V3 新增成就系统 - }; - } -} -``` - ---- - -**文档版本**: 1.0.0 -**更新日期**: 2026-01-12 \ No newline at end of file diff --git a/docs/api-reference/godot-api.md b/docs/api-reference/godot-api.md deleted file mode 100644 index 2a384ae1..00000000 --- a/docs/api-reference/godot-api.md +++ /dev/null @@ -1,951 +0,0 @@ -# GFramework.Godot API 参考 - -> GFramework.Godot 模块的完整 API 参考文档,包含 Godot 特定扩展和集成的详细说明。 - -## 📋 目录 - -- [架构集成](#架构集成) -- [Node 扩展方法](#node-扩展方法) -- [信号系统](#信号系统) -- [节点池化](#节点池化) -- [资源管理](#资源管理) -- [日志系统](#日志系统) -- [池化管理](#池化管理) -- [音频系统](#音频系统) - -## 架构集成 - -### AbstractArchitecture - -Godot 特定的架构基类,继承自 Core.Architecture。 - -#### 新增方法 - -```csharp -// Godot 模块安装 -protected void InstallGodotModule(IGodotModule module); - -protected void InstallGodotModule() where T : IGodotModule, new(); -``` - -#### 使用示例 - -```csharp -public class GameArchitecture : AbstractArchitecture -{ - protected override void Init() - { - RegisterModel(new PlayerModel()); - RegisterSystem(new CombatSystem()); - RegisterUtility(new StorageUtility()); - } - - protected override void InstallModules() - { - InstallGodotModule(new AudioModule()); - InstallGodotModule(new InputModule()); - } -} -``` - -### IGodotModule - -Godot 模块接口,继承自 IArchitectureModule。 - -#### 属性 - -```csharp -Node Node { get; } -``` - -#### 方法 - -```csharp -void Install(IArchitecture architecture); -void OnAttach(Architecture architecture); -void OnDetach(Architecture architecture); -void OnPhase(ArchitecturePhase phase, IArchitecture architecture); -``` - -#### 使用示例 - -```csharp -public class AudioModule : AbstractGodotModule -{ - // 模块节点本身 - public override Node Node => this; - - private AudioStreamPlayer _musicPlayer; - - public override void Install(IArchitecture architecture) - { - // 注册音频系统 - architecture.RegisterSystem(new AudioSystem()); - architecture.RegisterUtility(new AudioUtility()); - } - - public override void OnAttach(Architecture architecture) - { - // 创建音频播放器 - _musicPlayer = new AudioStreamPlayer(); - AddChild(_musicPlayer); - - Logger.Info("Audio module attached"); - } - - public override void OnDetach(Architecture architecture) - { - // 清理音频播放器 - _musicPlayer?.QueueFree(); - - Logger.Info("Audio module detached"); - } - - public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) - { - switch (phase) - { - case ArchitecturePhase.Ready: - PlayBackgroundMusic(); - break; - } - } - - private void PlayBackgroundMusic() - { - var music = GD.Load("res://audio/background.ogg"); - _musicPlayer.Stream = music; - _musicPlayer.Play(); - } -} -``` - -### AbstractGodotModule - -Godot 模块抽象基类,实现了 IGodotModule 接口。 - -#### 使用示例 - -```csharp -[ContextAware] -[Log] -public partial class SaveModule : AbstractGodotModule -{ - private Timer _autoSaveTimer; - - public override void Install(IArchitecture architecture) - { - architecture.RegisterSystem(new SaveSystem()); - architecture.RegisterUtility(new SaveUtility()); - } - - public override void OnAttach(Architecture architecture) - { - // 创建自动保存计时器 - _autoSaveTimer = new Timer(); - _autoSaveTimer.WaitTime = 300; // 5分钟 - _autoSaveTimer.Timeout += OnAutoSave; - AddChild(_autoSaveTimer); - _autoSaveTimer.Start(); - - Logger.Info("Save module attached with auto-save enabled"); - } - - private void OnAutoSave() - { - var saveSystem = Context.GetSystem(); - saveSystem.SaveGame("autosave"); - Logger.Debug("Auto-save completed"); - } -} -``` - -## Node 扩展方法 - -### 安全节点获取 - -#### GetNodeX() - -安全获取子节点,自动类型转换和 null 检查。 - -```csharp -public static T GetNodeX(this Node node, string path) where T : class; -``` - -#### 使用示例 - -```csharp -// 安全获取节点 -var player = GetNodeX("Player"); -var healthBar = GetNodeX("UI/HealthBar"); - -// 如果节点不存在或类型不匹配,会抛出异常 -// 使用时不需要额外的 null 检查 -``` - -#### GetChildX() - -安全查找子节点,支持递归查找。 - -```csharp -public static T GetChildX(this Node node, string path) where T : class; -``` - -#### 使用示例 - -```csharp -// 递归查找子节点 -var player = FindChildX("Player"); -var sprite = FindChildX("Enemy/Sprite"); -``` - -### 节点验证 - -#### IsValidNode() - -检查节点是否有效且在场景树中。 - -```csharp -public static bool IsValidNode(this Node node); -``` - -#### IsInvalidNode() - -检查节点是否无效或不在场景树中。 - -```csharp -public static bool IsInvalidNode(this Node node); -``` - -#### 使用示例 - -```csharp -Node someNode = GetNode("SomeNode"); - -if (IsValidNode(someNode)) -{ - someNode.DoSomething(); -} - -if (IsInvalidNode(someNode)) -{ - // 节点无效,需要重新获取 -} -``` - -### 安全节点操作 - -#### AddChildX() - -安全添加子节点,包含验证。 - -```csharp -public static void AddChildX(this Node parent, Node child, bool forceReadableName = false, InternalMode internalMode = 0); -``` - -#### QueueFreeX() - -安全销毁节点,包含验证。 - -```csharp -public static void QueueFreeX(this Node node); -``` - -#### FreeX() - -立即销毁节点,谨慎使用。 - -```csharp -public static void FreeX(this Node node); -``` - -#### 使用示例 - -```csharp -// 创建并添加子节点 -var bullet = bulletScene.Instantiate(); -AddChildX(bullet); - -// 安全销毁节点 -bullet.QueueFreeX(); - -// 立即销毁(谨慎使用) -tempNode.FreeX(); -``` - -### 异步节点操作 - -#### WaitUntilReady() - -等待节点准备就绪。 - -```csharp -public static async Task WaitUntilReady(this Node node); -``` - -#### WaitUntil() - -等待条件满足。 - -```csharp -public static async Task WaitUntil(this Node node, Func condition); -``` - -#### WaitUntilTimeout() - -等待指定时间或条件满足。 - -```csharp -public static async Task WaitUntilTimeout(this Node node, float timeoutSeconds); -``` - -#### 使用示例 - -```csharp -// 等待节点准备就绪 -await this.WaitUntilReady(); - -// 等待条件满足 -await this.WaitUntil(() => SomeCondition()); - -// 等待 2 秒 -await this.WaitUntilTimeout(2.0f); -``` - -### 场景树操作 - -#### GetParentX() - -安全获取父节点。 - -```csharp -public static T GetParentX(this Node node) where T : class; -``` - -#### ForEachChild() - -遍历所有子节点。 - -```csharp -public static void ForEachChild(this Node node, Action action) where T : Node; -``` - -#### 使用示例 - -```csharp -// 获取父节点 -var parent = GetParentX(); - -// 遍历所有子节点 -this.ForEachChild(child => { - if (child is Sprite2D sprite) - { - ProcessSprite(sprite); - } -}); -``` - -## 信号系统 - -### SignalBuilder - -信号构建器,提供流畅的信号连接 API。 - -#### 构造函数 - -```csharp -public SignalBuilder(Node node); -``` - -#### 连接方法 - -```csharp -public SignalBuilder Connect(Callable callable); -public SignalBuilder Connect(Callable callable); -public SignalBuilder Connect(Callable callable); -public SignalBuilder Connect(Callable callable); -public SignalBuilder Connect(Callable callable); -``` - -#### 配置方法 - -```csharp -public SignalBuilder WithFlags(ConnectFlags flags); -public SignalBuilder CallImmediately(); -``` - -#### 生命周期方法 - -```csharp -public SignalBuilder UnRegisterWhenNodeExitTree(Node node); -public SignalBuilder AddToUnregisterList(IUnRegisterList unregisterList); -``` - -#### 使用示例 - -```csharp -// 基础连接 -this.CreateSignalBuilder(Button.SignalName.Pressed) - .Connect(OnButtonPressed) - .UnRegisterWhenNodeExitTree(this); - -// 带标志的连接 -this.CreateSignalBuilder(Timer.SignalName.Timeout) - .WithFlags(ConnectFlags.OneShot) - .Connect(OnTimerTimeout); - -// 立即调用连接 -this.CreateSignalBuilder(CustomSignal.SignalName.CustomEvent) - .CallImmediately() - .Connect(OnCustomEvent); - -// 多参数连接 -this.CreateSignalBuilder() - .AddSignal(SignalName.Parameter1, OnParameter1) - .AddSignal(SignalName.Parameter2, OnParameter2) - .AddSignal(SignalName.Parameter3, OnParameter3) - .UnRegisterWhenNodeExitTree(this); -``` - -### SignalExtension - -信号扩展方法,简化信号创建。 - -#### CreateSignalBuilder() - -创建信号构建器。 - -```csharp -public static SignalBuilder CreateSignalBuilder(this Node node, string signalName); -``` - -#### ConnectSignal() - -直接连接信号。 - -```csharp -public static IUnRegister ConnectSignal(this Node node, string signalName, Callable callable); -public static IUnRegister ConnectSignal(this Node node, string signalName, Callable callable); -``` - -#### 使用示例 - -```csharp -// 创建信号构建器连接 -this.CreateSignalBuilder(Button.SignalName.Pressed) - .Connect(OnButtonPressed); - -// 直接连接信号 -this.ConnectSignal(Button.SignalName.Pressed, Callable.From(OnButtonPressed)); - -// 多参数信号连接 -this.ConnectSignal(ComplexSignal.SignalName.DataChanged, - Callable.From(OnDataChanged)); -``` - -### 信号与框架事件桥接 - -#### SignalEventBridge - -信号事件桥接器,将 Godot 信号转换为框架事件。 - -#### 使用示例 - -```csharp -[ContextAware] -[Log] -public partial class GameController : Node, IController -{ - public override void _Ready() - { - // Godot 信号 -> 框架事件 - this.CreateSignalBuilder(Button.SignalName.Pressed) - .Connect(() => { - Context.SendEvent(new ButtonClickEvent { ButtonId = "start" }); - }) - .UnRegisterWhenNodeExitTree(this); - - // 框架事件 -> Godot 信号 - this.RegisterEvent(OnPlayerHealthChange) - .UnRegisterWhenNodeExitTree(this); - } - - private void OnPlayerHealthChange(PlayerHealthChangeEvent e) - { - // 更新 Godot UI - var healthBar = GetNode("UI/HealthBar"); - healthBar.Value = (float)e.NewHealth / e.MaxHealth * 100; - - // 发送 Godot 信号 - EmitSignal(SignalName.HealthUpdated, e.NewHealth, e.MaxHealth); - } - - [Signal] - public delegate void HealthUpdatedEventHandler(int newHealth, int maxHealth); -} -``` - -## 节点池化 - -### AbstractNodePoolSystem - -节点池化系统基类,TNode 必须是 Node。 - -#### 抽象方法 - -```csharp -protected abstract TNode CreateItem(TKey key); -protected abstract void OnSpawn(TNode item, TKey key); -protected abstract void OnDespawn(TNode item); -protected abstract bool CanDespawn(TNode item); -``` - -#### 公共方法 - -```csharp -public TNode Spawn(TKey key); -public void Despawn(TNode item); -public void DespawnAll(); -public int GetActiveCount(TKey key); -public int GetTotalCount(TKey key); -``` - -#### 使用示例 - -```csharp -public class BulletPoolSystem : AbstractNodePoolSystem -{ - private readonly Dictionary _scenes = new(); - - public BulletPoolSystem() - { - _scenes["player"] = GD.Load("res://scenes/PlayerBullet.tscn"); - _scenes["enemy"] = GD.Load("res://scenes/EnemyBullet.tscn"); - } - - protected override Bullet CreateItem(string key) - { - if (_scenes.TryGetValue(key, out var scene)) - { - return scene.Instantiate(); - } - throw new ArgumentException($"Unknown bullet type: {key}"); - } - - protected override void OnSpawn(Bullet item, string key) - { - item.Reset(); - item.Position = Vector2.Zero; - item.Visible = true; - item.SetCollisionLayerValue(1, true); - item.SetCollisionMaskValue(1, true); - } - - protected override void OnDespawn(Bullet item) - { - item.Visible = false; - item.SetCollisionLayerValue(1, false); - item.SetCollisionMaskValue(1, false); - - // 移除父节点 - item.GetParent()?.RemoveChild(item); - } - - protected override bool CanDespawn(Bullet item) - { - return !item.IsActive && item.GetParent() != null; - } -} - -// 使用示例 -var bulletPool = new BulletPoolSystem(); - -// 从池中获取子弹 -var bullet = bulletPool.Spawn("player"); -AddChild(bullet); - -// 回收子弹 -bulletPool.Despawn(bullet); -``` - -### IPoolableNode - -可池化节点接口。 - -```csharp -public interface IPoolableNode -{ - void Reset(); - void SetActive(bool active); - bool IsActive { get; } -} -``` - -#### 使用示例 - -```csharp -public partial class Bullet : Area2D, IPoolableNode -{ - [Export] public float Speed { get; set; } = 500.0f; - [Export] public float Damage { get; set; } = 10.0f; - - private bool _isActive; - - public bool IsActive => _isActive; - - public void Reset() - { - Position = Vector2.Zero; - Rotation = 0; - Velocity = Vector2.Zero; - _isActive = false; - } - - public void SetActive(bool active) - { - _isActive = active; - Visible = active; - SetProcess(active); - } - - public override void _Ready() - { - BodyEntered += OnBodyEntered; - } - - private void OnBodyEntered(Node body) - { - if (body is Enemy enemy && _isActive) - { - enemy.TakeDamage(Damage); - // 子弹命中敌人,回收到池中 - var bulletPool = GetNode("/root/BulletPool"); - bulletPool?.Despawn(this); - } - } - - public override void _Process(double delta) - { - if (!_isActive) return; - - Position += Transform.X * (float)(Speed * delta); - } -} -``` - -## 资源管理 - -### ResourceLoadUtility - -资源加载工具类,简化和缓存 Godot 资源加载。 - -#### 构造函数 - -```csharp -public ResourceLoadUtility(); -``` - -#### 方法 - -```csharp -public T LoadResource(string path) where T : Resource; -public async Task LoadResourceAsync(string path) where T : Resource; -public void PreloadResource(string path) where T : Resource; -public bool HasResource(string path) where T : Resource; -``` - -#### 使用示例 - -```csharp -var resourceLoader = new ResourceLoadUtility(); - -// 同步加载资源 -var playerTexture = resourceLoader.LoadResource("res://textures/player.png"); -var playerScene = resourceLoader.LoadResource("res://scenes/Player.tscn"); - -// 异步加载资源 -var musicStream = await resourceLoader.LoadResourceAsync("res://audio/background.ogg"); - -// 预加载资源 -resourceLoader.PreloadResource("res://textures/enemy.png"); -resourceLoader.PreloadResource("res://scenes/enemy.tscn"); -``` - -### AbstractResourceFactoryUtility - -抽象资源工厂工具基类。 - -#### 抽象方法 - -```csharp -protected abstract void RegisterFactories(); -protected abstract T CreateFactory(string path, Dictionary metadata = null) where T : class; -``` - -#### 使用示例 - -```csharp -public class GameResourceFactory : AbstractResourceFactoryUtility -{ - protected override void RegisterFactories() - { - RegisterFactory("res://data/players/{id}.json"); - RegisterFactory("res://data/weapons/{id}.json"); - RegisterFactory("res://data/levels/{id}.json"); - } - - public PlayerData CreatePlayer(string playerId) - { - return CreateFactory($"res://data/players/{playerId}.json"); - } - - public WeaponConfig CreateWeapon(string weaponId) - { - var metadata = new Dictionary - { - ["weaponId"] = weaponId, - ["loadTime"] = DateTime.Now - }; - - return CreateFactory($"res://data/weapons/{weaponId}.json", metadata); - } -} -``` - -## 日志系统 - -### GodotLogger - -Godot 特定的日志实现。 - -#### 构造函数 - -```csharp -public GodotLogger(string categoryName); -public GodotLogger(string categoryName, LogLevel minLevel); -``` - -#### 方法 - -```csharp -public void Log(LogLevel level, T message, params object[] args); -public void Debug(T message, params object[] args); -public void Info(T message, params object[] args); -public void Warning(T message, params object[] args); -public void Error(T message, params object[] args); -public void Error(Exception exception, T message, params object[] args); -``` - -#### 使用示例 - -```csharp -// 创建日志器 -var logger = new GodotLogger("GameController"); - -// 不同级别的日志 -logger.Debug("Player position: {0}", player.Position); -logger.Info("Game started"); -logger.Warning("Low health: {0}", player.Health); -logger.Error("Failed to load resource: {0}", resourcePath); - -// 带异常的错误日志 -logger.Error(exception, "An error occurred while processing player input"); -``` - -### GodotLoggerFactory - -Godot 日志工厂。 - -#### 方法 - -```csharp -public ILogger CreateLogger(string categoryName); -public ILogger CreateLogger(Type type); -``` - -#### 使用示例 - -```csharp -var factory = new GodotLoggerFactory(); - -// 创建日志器 -var gameLogger = factory.CreateLogger("GameController"); -var playerLogger = factory.CreateLogger(typeof(PlayerController)); - -// 在架构中使用 -public class GameArchitecture : AbstractArchitecture -{ - protected override void Init() - { - LoggerProperties = new LoggerProperties - { - LoggerFactoryProvider = new GodotLoggerFactoryProvider(), - MinLevel = LogLevel.Info - }; - - RegisterSystem(new GameController()); - } -} -``` - -## 池化管理 - -### AbstractObjectPool - -通用对象池基类。 - -#### 构造函数 - -```csharp -public AbstractObjectPool( - Func createFunc, - Action actionOnGet = null, - Action actionOnRelease = null, - bool collectionCheck = false -); -``` - -#### 方法 - -```csharp -public T Get(); -public void Release(T item); -public void Clear(); -public int CountInactive { get; } -public int CountAll { get; } -``` - -#### 使用示例 - -```csharp -// 创建对象池 -var explosionPool = new AbstractObjectPool( - createFunc: () => new ExplosionEffect(), - actionOnGet: effect => effect.Reset(), - actionOnRelease: effect => Cleanup() -); - -// 使用对象池 -var effect = explosionPool.Get(); -effect.Play(effect.Position); - -// 回收对象 -explosionPool.Release(effect); -``` - ---- - -## 音频系统 - -### AudioSystem - -音频系统,管理音乐和音效播放。 - -#### 使用示例 - -```csharp -[ContextAware] -[Log] -public partial class AudioSystem : AbstractSystem -{ - private AudioStreamPlayer _musicPlayer; - private readonly Dictionary _soundCache = new(); - - protected override void OnInit() - { - InitializeAudioPlayers(); - CacheSounds(); - - // 监听音频事件 - this.RegisterEvent(OnPlaySound); - this.RegisterEvent(OnPlayMusic); - this.RegisterEvent(OnStopMusic); - this.RegisterEvent(OnSetVolume); - } - - private void InitializeAudioPlayers() - { - _musicPlayer = new AudioStreamPlayer(); - AddChild(_musicPlayer); - - // 配置音乐播放器 - _musicPlayer.Bus = "Music"; - } - - private void CacheSounds() - { - var soundPaths = new[] - { - "res://audio/jump.wav", - "res://audio/attack.wav", - "res://audio/hurt.wav", - "res://audio/victory.wav" - }; - - foreach (var path in soundPaths) - { - var sound = GD.Load(path); - var soundName = Path.GetFileNameWithoutExtension(path); - _soundCache[soundName] = sound; - } - } - - private void OnPlaySound(PlaySoundEvent e) - { - if (_soundCache.TryGetValue(e.SoundName, out var sound)) - { - PlaySound(sound); - } - else - { - Logger.Warning($"Sound not found: {e.SoundName}"); - } - } - - private void PlayMusic(PlayMusicEvent e) - { - var music = GD.Load(e.MusicPath); - _musicPlayer.Stream = music; - _musicPlayer.Play(); - - Logger.Info($"Playing music: {e.MusicPath}"); - } - - private void OnStopMusic(StopMusicEvent e) - { - _musicPlayer.Stop(); - Logger.Info("Music stopped"); - } - - private void OnSetVolume(SetVolumeEvent e) - { - AudioServer.SetBusVolume(e.BusName, e.Volume); - Logger.Info($"Set volume for bus {e.BusName}: {e.Volume}"); - } - - private void PlaySound(AudioStream sound) - { - var player = new AudioStreamPlayer(); - player.Stream = sound; - player.Bus = "SFX"; - player.Play(); - - // 自动清理播放器 - this.CreateSignalBuilder(player.SignalName.Finished) - .WithFlags(ConnectFlags.OneShot) - .Connect(() => player.QueueFree()) - .UnRegisterWhenNodeExitTree(this); - } -} - -// 音频事件 -public struct PlaySoundEvent { public string SoundName; } -public struct PlayMusicEvent { public string MusicPath; } -public struct StopMusicEvent { } -public struct SetVolumeEvent { public string BusName; public float Volume; } -``` - ---- - -**文档版本**: 1.0.0 -**更新日期**: 2026-01-12 \ No newline at end of file diff --git a/docs/api-reference/source-generators-api.md b/docs/api-reference/source-generators-api.md deleted file mode 100644 index b4672c65..00000000 --- a/docs/api-reference/source-generators-api.md +++ /dev/null @@ -1,601 +0,0 @@ -# GFramework.SourceGenerators API 参考 - -> GFramework.SourceGenerators 模块的完整 API 参考文档,包含所有源代码生成器的详细说明。 - -## 📋 目录 - -- [日志生成器](#日志生成器) -- [上下文感知生成器](#上下文感知生成器) -- [枚举扩展生成器](#枚举扩展生成器) -- [诊断系统](#诊断系统) -- [通用工具](#通用工具) - -## 日志生成器 - -### [Log] 属性 - -为标记的类自动生成 ILogger 字段。 - -#### 构造函数参数 - -```csharp -[Log( - string fieldName = "Logger", // 生成的字段名 - AccessModifier accessModifier = AccessModifier.Private, // 访问修饰符 - bool isStatic = true, // 是否生成静态字段 - string loggerName = null, // 自定义日志器名称 - LogLevel minLevel = LogLevel.None, // 最小日志级别 - bool includeLoggerInterface = false // 是否包含 ILogger 接口 - bool enableConditionalCompilation = false // 是否启用条件编译 - string loggerInterfaceName = "ILogger" // 日志接口名称 - bool suppressAutoGeneratedAttribute = false // 是否抑制自动生成属性 - string category = null // 日志类别 - bool forceContextualLogging = false // 是否强制上下文日志 - bool enableStructuredLogging = false // 是否启用结构化日志 - string loggerFactoryName = null // 日志工厂名称 - string logMessagePrefix = null // 日志消息前缀 - bool enablePerformanceLogging = false // 是否启用性能日志 - bool enableAsyncLogging = false // 是否启用异步日志 - bool enableFluentLogging = false // 是否启用流畅日志 - bool enableScopedLogging = false // 是否启用作用域日志 -)] -``` - -#### 生成的代码示例 - -```csharp -// 默认配置生成的代码 -public partial class MyClass -{ - private static readonly ILogger Logger = - LoggerFactory.Create(builder => builder - .AddConsole() - .SetMinimumLevel(LogLevel.Information) - .CreateLogger()); -} - -// 自定义配置生成的代码 -[Log( - fieldName = "CustomLogger", - accessModifier = AccessModifier.Public, - isStatic = false, - loggerName = "Custom.MyClass", - minLevel = LogLevel.Debug, - includeLoggerInterface = true -)] -public partial class CustomClass : ILogger -{ - public ILogger CustomLogger { get; } - - static CustomClass() - { - CustomLogger = LoggerFactory.Create(builder => builder - .AddConsole() - .SetMinimumLevel(LogLevel.Debug) - .CreateLogger()); - } -} -``` - -#### 支持的方法 - -生成的 Logger 支持以下方法: - -```csharp -// 基础日志方法 -Logger.LogDebug("Debug message"); -Logger.LogInformation("Info message"); -Logger.LogWarning("Warning message"); -Logger.LogError("Error message"); -Logger.LogCritical("Critical message"); - -// 带格式化的日志方法 -Logger.LogInformation("Player {Name} has {Health} health", playerName, playerHealth); - -// 异步日志方法 -await Logger.LogInformationAsync("Async message"); - -// 结构化日志 -Logger.LogInformation(new { PlayerName = "John", Health = 100 }, "Player {Name} has {Health} health"); -``` - -## 上下文感知生成器 - -### [ContextAware] 属性 - -为标记的类自动实现 IContextAware 接口。 - -#### 生成的代码示例 - -```csharp -[ContextAware] -public partial class MyClass : IContextAware -{ - private IContextAware.Context _context; - - public IContextAware.Context Context => _context ??= new LazyContext(this); - - public void SetContext(IContextAware.Context context) - { - _context = context; - } - - public IContextAware.Context GetContext() - { - return _context; - } -} - -// 使用延迟初始化 -[ContextAware(useLazy = true)] -public partial class LazyContextClass : IContextAware -{ - private Lazy _context; - - public IContextAware.Context Context => _context.Value; - - public void SetContext(IContextAware.Context context) - { - _context = new Lazy(() => context); - } -} - -// 带上下文验证 -[ContextAware(validateContext = true)] -public partial class ValidatedContextClass : IContextAware -{ - private IContextAware.Context _context; - - public IContextAware.Context Context => - { - get => _context; - private set - { - _context = value; - if (_context?.IsInvalid == true) - { - throw new InvalidOperationException("Context is invalid"); - } - } - } -} -``` - -#### 可用的方法 - -```csharp -// 获取模型 -var playerModel = Context.GetModel(); -var gameModel = Context.GetModel(); - -// 获取系统 -var combatSystem = Context.GetSystem(); -var audioSystem = Context.GetSystem(); - -// 获取工具 -var storageUtility = Context.GetUtility(); -var mathUtility = Context.GetUtility(); - -// 发送事件 -Context.SendEvent(); -Context.SendEvent(new DamageDealtEvent { Damage = 50 }); - -// 发送命令 -Context.SendCommand(new AttackCommand()); -var result = Context.SendCommand(new AttackCommand()); - -// 发送查询 -var canAttack = Context.SendQuery(); -var playerData = Context.SendQuery(); -``` - -## 枚举扩展生成器 - -### [GenerateEnumExtensions] 属性 - -为枚举类型自动生成扩展方法。 - -#### 构造函数参数 - -```csharp -[GenerateEnumExtensions( - bool generateIsMethods = true, // 是否生成 IsX() 方法 - bool generateHasMethod = true, // 是否生成 HasX() 方法 - bool generateInMethod = true, // 是否生成 In(params T[]) 方法 - bool generateTryParseMethod = false, // 是否生成 TryParse() 方法 - string customPrefix = "Is", // 自定义方法名前缀 - bool includeToString = false, // 是否生成 ToString 扩展 - bool generateAllMethod = false, // 是否生成 All() 方法 - bool generateCountMethod = false, // 是否生成 Count() 方法 - bool generateDescriptionMethod = false, // 是否生成 Description() 方法 - bool generateValuesMethod = false, // 是否生成 Values() 方法 - string customNamespace = null, // 自定义命名空间 - bool generateExtensionMethods = true, // 是否生成扩展方法 - bool generateFlagMethods = true, // 是否为位标志枚举生成方法 - bool generateValidationMethods = false, // 是否生成验证方法 - bool generateUtilityMethods = false, // 是否生成工具方法 - bool generateQueryableMethods = false, // 是否生成查询方法 - string customMethodNameFormat = null, // 自定义方法名格式 - bool generateDocumentation = false, // 是否生成文档注释 - bool generateExamples = false, // 是否生成使用示例 -)] -``` - -#### 普通枚举生成的代码示例 - -```csharp -[GenerateEnumExtensions] -public enum PlayerState -{ - Idle, - Walking, - Running, - Jumping, - Attacking -} - -// 生成的扩展方法 -public static class PlayerStateExtensions -{ - public static bool IsIdle(this PlayerState state) => state == PlayerState.Idle; - public static bool IsWalking(this PlayerState state) => state == PlayerState.Walking; - public static bool IsRunning(this PlayerState state) => state == PlayerState.Running; - public static bool IsJumping(this PlayerState state) => state == PlayerState.Jumping; - public static bool IsAttacking(this PlayerState state) => state == PlayerState.Attacking; - - public static bool In(this PlayerState state, params PlayerState[] values) - { - return values.Contains(state); - } -} -``` - -#### 位标志枚举生成的代码示例 - -```csharp -[GenerateEnumExtensions] -[Flags] -public enum PlayerPermissions -{ - None = 0, - CanMove = 1 << 0, - CanAttack = 1 << 1, - CanUseItems = 1 << 2, - CanChat = 1 << 3, - CanTrade = 1 << 4 -} - -// 生成的扩展方法 -public static class PlayerPermissionsExtensions -{ - public static bool HasCanMove(this PlayerPermissions permissions) => permissions.HasFlag(PlayerPermissions.CanMove); - public static bool HasCanAttack(this PlayerPermissions permissions) => permissions.HasFlag(PlayerPermissions.CanAttack); - public static bool HasCanUseItems(this PlayerPermissions permissions) => permissions.HasFlag(PlayerPermissions.CanUseItems); - - public static bool HasAny(this PlayerPermissions permissions, params PlayerPermissions[] flags) - { - return flags.Any(flag => permissions.HasFlag(flag)); - } - - public static bool HasAll(this PlayerPermissions permissions, params PlayerPermissions[] flags) - { - return flags.All(flag => permissions.HasFlag(flag)); - } - - public static PlayerPermissions Add(this PlayerPermissions permissions, PlayerPermissions other) - { - return permissions | other; - } - - public static PlayerPermissions Remove(this PlayerPermissions permissions, PlayerPermissions other) - { - return permissions & ~other; - } - - public static PlayerPermissions Toggle(this PlayerPermissions permissions, PlayerPermissions flag) - { - return permissions ^ flag; - } -} -``` - -#### 高级功能示例 - -```csharp -[GenerateEnumExtensions( - customPrefix = "Is", - includeToString = true, - generateDescriptionMethod = true, - generateUtilityMethods = true -)] -public enum GameState -{ - [Description("游戏菜单状态")] - Menu, - - [Description("游戏进行中")] - Playing, - - [Description("游戏暂停")] - Paused, - - [Description("游戏结束")] - GameOver -} - -// 生成的扩展方法包含更多功能 -public static class GameStateExtensions -{ - // IsX() 方法 - public static bool IsMenu(this GameState state) => state == GameState.Menu; - public static bool IsPlaying(this GameState state) => state == GameState.Playing; - public static bool IsPaused(this GameState state) => state == GameState.Paused; - public static bool IsGameOver(this GameState state) => state == GameState.GameOver; - - // ToString() 扩展 - public static string ToDisplayString(this GameState state) - { - return state switch - { - GameState.Menu => "游戏菜单", - GameState.Playing => "游戏进行中", - GameState.Paused => "游戏暂停", - GameState.GameOver => "游戏结束" - }; - } - - // Description() 扩展 - public static string GetDescription(this GameState state) - { - return typeof(GameState) - .GetMember(state.ToString()) - ?.GetCustomAttribute() - ?.Description ?? state.ToString(); - } - - // 工具方法 - public static bool IsFinalState(this GameState state) => state == GameState.GameOver; - public static bool IsPlayingState(this GameState state) => state == GameState.Playing; - public static bool CanPause(this GameState state) => state == GameState.Playing; - - // 验证方法 - public static bool IsValidTransition(this GameState from, GameState to) - { - return (from, to) switch - { - (GameState.Menu, GameState.Playing) => true, - (GameState.Playing, GameState.Paused) => true, - (GameState.Paused, GameState.Playing) => true, - (GameState.Playing, GameState.GameOver) => true, - _ => false - }; - } -} -``` - -## 诊断系统 - -### GF_Logging_001 - 日志字段名冲突 - -当使用 `[Log]` 属性时,如果已存在同名字段或属性,会触发此诊断。 - -#### 示例 - -```csharp -// 错误示例:字段名冲突 -[Log(fieldName = "Logger")] -public partial class MyClass -{ - private readonly ILogger Logger; // ❌ 冲突! -} - -// 正确的解决方案 -[Log(fieldName = "CustomLogger")] -public partial class MyClass -{ - private readonly ILogger CustomLogger; // ✅ 使用不同的字段名 -} -``` - -#### 诊断消息 - -``` -error GF_Logging_001: Logger field name 'Logger' conflicts with existing field in type 'MyClass' -``` - -### GF_Rule_001 - 上下文感知接口冲突 - -当使用 `[ContextAware]` 属性时,如果类型已经实现了 IContextAware 接口,会触发此诊断。 - -#### 示例 - -```csharp -// 错误示例:接口冲突 -[ContextAware] -public partial class MyClass : IContextAware // ❌ 冲突! -{ - public IContextAware.Context Context { get; set; } - public void SetContext(IContextAware.Context context) { } - public IContextAware.Context GetContext() { return Context; } -} - -// 正确的解决方案:移除手动实现 -[ContextAware] -public partial class MyClass // ✅ 让生成器实现接口 -{ - // 移除手动实现的代码 -} -``` - -#### 诊断消息 - -``` -error GF_Rule_001: Type 'MyClass' already implements 'IContextAware' interface. Remove the [ContextAware] attribute or manual implementation. -``` - -## 通用工具 - -### TypeHelper - -类型操作的工具类。 - -#### 主要方法 - -```csharp -public static class TypeHelper -{ - // 类型检查 - public static bool IsNullable(Type type); - public static bool IsGenericType(Type type); - public static bool IsValueType(Type type); - public static bool IsEnum(Type type); - - // 泛型类型信息 - public static Type GetGenericTypeDefinition(Type type); - public static Type[] GetGenericArguments(Type type); - public static bool ImplementsInterface(Type type, Type interfaceType); - - // 属性信息 - public static PropertyInfo[] GetProperties(Type type, BindingFlags flags = BindingFlags.Public | BindingFlags.Instance); - public static PropertyInfo GetProperty(Type type, string name, BindingFlags flags = BindingFlags.Public | BindingFlags.Instance); - public static bool HasProperty(Type type, string name); - - // 方法信息 - public static MethodInfo[] GetMethods(Type type, BindingFlags flags = BindingFlags.Public | BindingFlags.Instance); - public static MethodInfo GetMethod(Type type, string name, BindingFlags flags = BindingFlags.Public | BindingFlags.Instance); - public static bool HasMethod(Type type, string name); - - // 构造函数信息 - public static ConstructorInfo[] GetConstructors(Type type, BindingFlags flags = BindingFlags.Public | BindingFlags.Instance); - public static ConstructorInfo GetConstructor(Type type, Type[] parameterTypes); - - // 可实例化检查 - public static bool HasParameterlessConstructor(Type type); - public static bool HasConstructor(Type type, params Type[] parameterTypes); -} -``` - -### SymbolHelper - -符号操作的工具类。 - -#### 主要方法 - -```csharp -public static class SymbolHelper -{ - // 获取符号信息 - public static ITypeSymbol GetSymbolInfo(INamedTypeSymbol symbol, Compilation compilation); - public static IMethodSymbol GetMethodInfo(IMethodSymbol symbol); - public static IPropertySymbol GetPropertyInfo(IPropertySymbol symbol); - public static IEventSymbol GetEventInfo(IEventSymbol symbol); - - // 符号比较 - public static bool IsSameSymbol(ISymbol symbol1, ISymbol symbol2); - public static string GetFullyQualifiedName(ISymbol symbol); - public static string GetDocumentationCommentXml(ISymbol symbol); - - // 符号查找 - public static IEnumerable GetMembers(INamedTypeSymbol symbol); - public static ISymbol GetMember(INamedTypeSymbol symbol, string name); - public static IEnumerable GetAttributes(ISymbol symbol); - public static T GetAttribute(ISymbol symbol); - public static bool HasAttribute(ISymbol symbol); -} -``` - -### CompilationHelper - -编译操作的工具类。 - -#### 主要方法 - -```csharp -public static class CompilationHelper -{ - // 获取编译 - public static Compilation GetCompilation(Compilation startingCompilation); - public static Compilation GetCompilation(SyntaxTree syntaxTree); - public static Compilation GetCompilation(IEnumerable syntaxTrees); - - // 获取语义模型 - public static SemanticModel GetSemanticModel(Compilation compilation); - public static SemanticModel GetSemanticModel(SyntaxTree syntaxTree); - - // 符号查找 - public static INamedTypeSymbol GetDeclaredTypeSymbol(Compilation compilation, string name); - public static IMethodSymbol GetDeclaredMethodSymbol(Compilation compilation, string name); - public static IPropertySymbol GetDeclaredPropertySymbol(Compilation compilation, string name); - - // 类型解析 - public static INamedTypeSymbol ResolveType(Compilation compilation, string metadataName); - public static ITypeSymbol ResolveType(Compilation compilation, Type type); - public static IMethodSymbol ResolveMethod(Compilation compilation, string methodFullName, params ITypeSymbol[] parameterTypes); - - // 编译检查 - public static bool HasCompilationErrors(Compilation compilation); - public static IEnumerable GetCompilationDiagnostics(Compilation compilation); - public static string GetCompilationErrors(Compilation compilation); -} -``` - ---- - -## 配置示例 - -### 项目文件配置 - -```xml - - - net6.0 - enable - true - Generated - - - - - - - - - - - -``` - -### 编辑器配置 - -```json -{ - "sourceGenerators": { - "debug": true, - "logLevel": "Information", - "outputDirectory": "Generated", - "enableDocumentation": true, - "enablePerformanceLogging": false - }, - "loggingGenerator": { - "defaultFieldName": "Logger", - "defaultAccessLevel": "Private", - "defaultStatic": true, - "defaultMinLevel": "None" - }, - "contextAwareGenerator": { - "useLazyInit": false, - "validateContext": false - }, - "enumExtensionsGenerator": { - "generateIsMethods": true, - "generateHasMethod": true, - "generateInMethod": true, - "customPrefix": "Is" - } -} -``` - ---- - -**文档版本**: 1.0.0 -**更新日期**: 2026-01-12 \ No newline at end of file diff --git a/docs/api/toc.yml b/docs/api/toc.yml deleted file mode 100644 index 407dc09f..00000000 --- a/docs/api/toc.yml +++ /dev/null @@ -1,2 +0,0 @@ -- name: Namespaces - href: namespaces.md \ No newline at end of file diff --git a/docs/bun.lock b/docs/bun.lock new file mode 100644 index 00000000..1b29620c --- /dev/null +++ b/docs/bun.lock @@ -0,0 +1,335 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "devDependencies": { + "@types/node": "^25.2.3", + "vitepress": "^2.0.0-alpha.16", + "vue": "^3.5.28", + }, + }, + }, + "packages": { + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@docsearch/css": ["@docsearch/css@4.5.4", "", {}, "sha512-gzO4DJwyM9c4YEPHwaLV1nUCDC2N6yoh0QJj44dce2rcfN71mB+jpu3+F+Y/KMDF1EKV0C3m54leSWsraE94xg=="], + + "@docsearch/js": ["@docsearch/js@4.5.4", "", {}, "sha512-jEBsIklNTevznLZouB0y6SZcaT897gRHnGTzCcH32ibPZRVj/9FyuAM2LUTk61sFdOY79LH4V9rYsIdOe6Wq2g=="], + + "@docsearch/sidepanel-js": ["@docsearch/sidepanel-js@4.5.4", "", {}, "sha512-f4KE4cG+P09gJHQNfttfMNy+3gAGj8U0YEgiOOso0YCFI5nGoVvJQpxNMSPgXs4sG34A/oCfKhYwHJiqgHhxPw=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.70", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-CYNRCgN6nBTjN4dNkrBCjHXNR2e4hQihdsZUs/afUNFOWLSYjfihca4EFN05rRvDk4Xoy2n8tym6IxBZmcn+Qg=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], + + "@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA=="], + + "@shikijs/langs": ["@shikijs/langs@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA=="], + + "@shikijs/themes": ["@shikijs/themes@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g=="], + + "@shikijs/transformers": ["@shikijs/transformers@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/types": "3.22.0" } }, "sha512-E7eRV7mwDBjueLF6852n2oYeJYxBq3NSsDk+uyruYAXONv4U8holGmIrT+mPRJQ1J1SNOH6L8G19KRzmBawrFw=="], + + "@shikijs/types": ["@shikijs/types@3.22.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], + + "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], + + "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.28", "", { "dependencies": { "@vue/compiler-core": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA=="], + + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.28", "@vue/compiler-dom": "3.5.28", "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g=="], + + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g=="], + + "@vue/devtools-api": ["@vue/devtools-api@8.0.6", "", { "dependencies": { "@vue/devtools-kit": "^8.0.6" } }, "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA=="], + + "@vue/devtools-kit": ["@vue/devtools-kit@8.0.6", "", { "dependencies": { "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^2.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw=="], + + "@vue/devtools-shared": ["@vue/devtools-shared@8.0.6", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg=="], + + "@vue/reactivity": ["@vue/reactivity@3.5.28", "", { "dependencies": { "@vue/shared": "3.5.28" } }, "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw=="], + + "@vue/runtime-core": ["@vue/runtime-core@3.5.28", "", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ=="], + + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.28", "", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/runtime-core": "3.5.28", "@vue/shared": "3.5.28", "csstype": "^3.2.3" } }, "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA=="], + + "@vue/server-renderer": ["@vue/server-renderer@3.5.28", "", { "dependencies": { "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "vue": "3.5.28" } }, "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg=="], + + "@vue/shared": ["@vue/shared@3.5.28", "", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="], + + "@vueuse/core": ["@vueuse/core@14.2.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.1", "@vueuse/shared": "14.2.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ=="], + + "@vueuse/integrations": ["@vueuse/integrations@14.2.1", "", { "dependencies": { "@vueuse/core": "14.2.1", "@vueuse/shared": "14.2.1" }, "peerDependencies": { "async-validator": "^4", "axios": "^1", "change-case": "^5", "drauu": "^0.4", "focus-trap": "^7 || ^8", "fuse.js": "^7", "idb-keyval": "^6", "jwt-decode": "^4", "nprogress": "^0.2", "qrcode": "^1.5", "sortablejs": "^1", "universal-cookie": "^7 || ^8", "vue": "^3.5.0" }, "optionalPeers": ["async-validator", "axios", "change-case", "drauu", "focus-trap", "fuse.js", "idb-keyval", "jwt-decode", "nprogress", "qrcode", "sortablejs", "universal-cookie"] }, "sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA=="], + + "@vueuse/metadata": ["@vueuse/metadata@14.2.1", "", {}, "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw=="], + + "@vueuse/shared": ["@vueuse/shared@14.2.1", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw=="], + + "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mark.js": ["mark.js@8.11.1", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "minisearch": ["minisearch@7.2.0", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="], + + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + + "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], + + "shiki": ["shiki@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/engine-javascript": "3.22.0", "@shikijs/engine-oniguruma": "3.22.0", "@shikijs/langs": "3.22.0", "@shikijs/themes": "3.22.0", "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="], + + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vitepress": ["vitepress@2.0.0-alpha.16", "", { "dependencies": { "@docsearch/css": "^4.5.3", "@docsearch/js": "^4.5.3", "@docsearch/sidepanel-js": "^4.5.3", "@iconify-json/simple-icons": "^1.2.68", "@shikijs/core": "^3.21.0", "@shikijs/transformers": "^3.21.0", "@shikijs/types": "^3.21.0", "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^6.0.3", "@vue/devtools-api": "^8.0.5", "@vue/shared": "^3.5.27", "@vueuse/core": "^14.1.0", "@vueuse/integrations": "^14.1.0", "focus-trap": "^7.8.0", "mark.js": "8.11.1", "minisearch": "^7.2.0", "shiki": "^3.21.0", "vite": "^7.3.1", "vue": "^3.5.27" }, "peerDependencies": { "markdown-it-mathjax3": "^4", "oxc-minify": "*", "postcss": "^8" }, "optionalPeers": ["markdown-it-mathjax3", "oxc-minify", "postcss"], "bin": { "vitepress": "bin/vitepress.js" } }, "sha512-w1nwsefDVIsje7BZr2tsKxkZutDGjG0YoQ2yxO7+a9tvYVqfljYbwj5LMYkPy8Tb7YbPwa22HtIhk62jbrvuEQ=="], + + "vue": ["vue@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", "@vue/runtime-dom": "3.5.28", "@vue/server-renderer": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + } +} diff --git a/docs/index.md b/docs/index.md index 407f04ff..8b0c7e99 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,33 +1,42 @@ -# GFramework Documentation - -Welcome to the GFramework documentation! - -## Overview - -GFramework is a comprehensive game development framework built with clean architecture principles. It provides a solid foundation for building scalable and maintainable game applications. - -## Key Features - -- **Clean Architecture**: Well-defined separation of concerns -- **Modular Design**: Pluggable architecture modules -- **Event System**: Robust event-driven programming -- **Command Pattern**: Structured command execution -- **Property Binding**: Reactive property system -- **Coroutine Support**: Asynchronous programming utilities -- **IOC Container**: Dependency injection support -- **Logging System**: Comprehensive logging capabilities - -## Getting Started - -Check out the [API Documentation](api/) for detailed reference, or explore the specific modules in the navigation menu. - -## Modules - -- **Core**: Fundamental framework components -- **Game**: Game-specific extensions -- **Godot**: Godot engine integration -- **Source Generators**: Compile-time code generation - -## Contributing - -Please read our contributing guidelines before submitting pull requests. \ No newline at end of file +--- +layout: page +--- + + + +
+

🌐 Language Selection / 语言选择

+ +

+ Auto-redirecting... / 自动跳转中... +

+
\ No newline at end of file diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..453a06a5 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,13 @@ +{ + "dependencies": {}, + "devDependencies": { + "@types/node": "^25.2.3", + "vitepress": "^2.0.0-alpha.16", + "vue": "^3.5.28" + }, + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + } +} \ No newline at end of file diff --git a/docs/public/logo-icon.png b/docs/public/logo-icon.png new file mode 100644 index 00000000..93b56cc9 Binary files /dev/null and b/docs/public/logo-icon.png differ diff --git a/docs/public/logo.png b/docs/public/logo.png new file mode 100644 index 00000000..15ff1a8a Binary files /dev/null and b/docs/public/logo.png differ diff --git a/docs/toc.yml b/docs/toc.yml deleted file mode 100644 index 841496db..00000000 --- a/docs/toc.yml +++ /dev/null @@ -1,29 +0,0 @@ -- name: Home - href: index.md -- name: API Documentation - href: api/ - homepage: api/index.md -- name: Core Framework - items: - - name: Architecture - href: GFramework.Core/architecture/README.md - - name: Commands - href: GFramework.Core/command/README.md - - name: Events - href: GFramework.Core/events/README.md - - name: Properties - href: GFramework.Core/property/README.md - - name: Systems - href: GFramework.Core/system/README.md - - name: Models - href: GFramework.Core/model/README.md - - name: Queries - href: GFramework.Core/query/README.md - - name: Coroutines - href: GFramework.Core/coroutine/ - - name: Logging - href: GFramework.Core/logging/README.md - - name: IOC Container - href: GFramework.Core/ioc/README.md - - name: Pool System - href: GFramework.Core/pool/README.md \ No newline at end of file diff --git a/docs/zh-CN/abstractions/core-abstractions.md b/docs/zh-CN/abstractions/core-abstractions.md new file mode 100644 index 00000000..b013317b --- /dev/null +++ b/docs/zh-CN/abstractions/core-abstractions.md @@ -0,0 +1,192 @@ +# Core Abstractions + +> GFramework.Core.Abstractions 核心抽象接口定义 + +## 概述 + +GFramework.Core.Abstractions 包含了框架的所有核心接口定义,这些接口定义了组件之间的契约,实现了依赖倒置和面向接口编程。 + +## 核心接口 + +### IArchitecture + +应用程序架构接口: + +```csharp +public interface IArchitecture +{ + void Initialize(); + void Destroy(); + + T GetModel() where T : IModel; + T GetSystem() where T : ISystem; + T GetUtility() where T : IUtility; + + void RegisterModel(IModel model); + void RegisterSystem(ISystem system); + void RegisterUtility(IUtility utility); +} +``` + +### IModel + +数据模型接口: + +```csharp +public interface IModel +{ + void Init(); + void Dispose(); + + IArchitecture Architecture { get; } +} +``` + +### ISystem + +业务逻辑系统接口: + +```csharp +public interface ISystem +{ + void Init(); + void Dispose(); + + IArchitecture Architecture { get; } +} +``` + +### IController + +控制器接口: + +```csharp +public interface IController : IBelongToArchitecture +{ + void Init(); + void Dispose(); +} +``` + +### IUtility + +工具类接口: + +```csharp +public interface IUtility +{ +} +``` + +## 事件接口 + +### IEvent + +事件基接口: + +```csharp +public interface IEvent +{ +} +``` + +### IEventHandler + +事件处理器接口: + +```csharp +public interface IEventHandler where TEvent : IEvent +{ + void Handle(TEvent event); +} +``` + +## 命令查询接口 + +### ICommand + +命令接口: + +```csharp +public interface ICommand +{ + void Execute(); +} +``` + +### IQuery + +查询接口: + +```csharp +public interface IQuery +{ + TResult Execute(); +} +``` + +## 依赖注入接口 + +### IIocContainer + +IOC 容器接口: + +```csharp +public interface IIocContainer +{ + void Register() where TImplementation : TInterface; + void Register(TInterface instance); + TInterface Resolve(); + bool IsRegistered(); +} +``` + +## 生命周期接口 + +### ILifecycle + +组件生命周期接口: + +```csharp +public interface ILifecycle +{ + void OnInit(); + void OnDestroy(); +} +``` + +## 使用示例 + +### 通过接口实现依赖注入 + +```csharp +public class MyService : IMyService +{ + private readonly IArchitecture _architecture; + + public MyService(IArchitecture architecture) + { + _architecture = architecture; + } +} +``` + +### 自定义事件 + +```csharp +public class PlayerDiedEvent : IEvent +{ + public int PlayerId { get; set; } + public Vector2 Position { get; set; } +} +``` + +--- + +**相关文档**: + +- [Core 概述](../core) +- [Architecture](../core/architecture) +- [Events](../core/events) +- [Command](../core/command) +- [Query](../core/query) diff --git a/docs/zh-CN/abstractions/game-abstractions.md b/docs/zh-CN/abstractions/game-abstractions.md new file mode 100644 index 00000000..4fc3c419 --- /dev/null +++ b/docs/zh-CN/abstractions/game-abstractions.md @@ -0,0 +1,113 @@ +# Game Abstractions + +> GFramework.Game.Abstractions 游戏模块抽象接口定义 + +## 概述 + +GFramework.Game.Abstractions 包含了游戏特定功能的抽象接口,这些接口定义了游戏开发中的通用契约。 + +## 存档接口 + +### ISaveSystem + +存档系统接口: + +```csharp +public interface ISaveSystem +{ + void Save(string slotId, SaveData data); + SaveData Load(string slotId); + bool HasSave(string slotId); + void Delete(string slotId); + List GetAllSaveSlots(); +} +``` + +### ISaveData + +存档数据接口: + +```csharp +public interface ISaveData +{ + int Version { get; } + DateTime Timestamp { get; } + void Validate(); +} +``` + +## 设置接口 + +### IGameSettings + +游戏设置接口: + +```csharp +public interface IGameSettings +{ + AudioSettings Audio { get; } + GraphicsSettings Graphics { get; } + InputSettings Input { get; } + + void Save(); + void Load(); + void ResetToDefaults(); +} +``` + +## 场景管理接口 + +### ISceneManager + +场景管理器接口: + +```csharp +public interface ISceneManager +{ + void SwitchScene() where TScene : IScene; + Task SwitchSceneAsync() where TScnee : IScene; + + void PushScene() where TScene : IScene; + void PopScene(); + + IScene CurrentScene { get; } +} +``` + +### IScene + +场景接口: + +```csharp +public interface IScene +{ + void OnEnter(); + void OnExit(); + void OnUpdate(float delta); +} +``` + +## 资源管理接口 + +### IAssetManager + +资源管理器接口: + +```csharp +public interface IAssetManager +{ + T Load(string path) where T : Resource; + void Preload(string path) where T : Resource; + void Unload(string path); + bool IsLoaded(string path); +} +``` + +--- + +**相关文档**: + +- [Game 概述](../game) +- [Core Abstractions](./core-abstractions) +- [存档系统](../game/storage) +- [场景管理](../game/scene-management) diff --git a/docs/best-practices/architecture-patterns.md b/docs/zh-CN/best-practices/architecture-patterns.md similarity index 100% rename from docs/best-practices/architecture-patterns.md rename to docs/zh-CN/best-practices/architecture-patterns.md diff --git a/GFramework.Core/architecture/README.md b/docs/zh-CN/core/architecture.md similarity index 97% rename from GFramework.Core/architecture/README.md rename to docs/zh-CN/core/architecture.md index 7c0550eb..43638b43 100644 --- a/GFramework.Core/architecture/README.md +++ b/docs/zh-CN/core/architecture.md @@ -495,13 +495,13 @@ var architecture = new GameArchitecture(configuration: config); ## 相关包 -- [`command`](../command/README.md) - 命令模式实现 -- [`query`](../query/README.md) - 查询模式实现 -- [`events`](../events/README.md) - 事件系统 -- [`ioc`](../ioc/README.md) - IoC 容器 -- [`model`](../model/README.md) - 数据模型 -- [`system`](../system/README.md) - 业务系统 -- [`utility`](../utility/README.md) - 工具类 +- [`command`](./command.md) - 命令模式实现 +- [`query`](./query.md) - 查询模式实现 +- [`events`](./events.md) - 事件系统 +- [`ioc`](./ioc.md) - IoC 容器 +- [`model`](./model.md) - 数据模型 +- [`system`](./system.md) - 业务系统 +- [`utility`](./utility.md) - 工具类 - **GFramework.Godot** - Godot 特定集成(GodotNode 扩展、GodotLogger 等) --- diff --git a/GFramework.Core/command/README.md b/docs/zh-CN/core/command.md similarity index 94% rename from GFramework.Core/command/README.md rename to docs/zh-CN/core/command.md index a84e49a2..701281da 100644 --- a/GFramework.Core/command/README.md +++ b/docs/zh-CN/core/command.md @@ -297,12 +297,12 @@ public class MoveCommand : AbstractCommand, IUndoableCommand ## 相关包 -- [`architecture`](../architecture/README.md) - 架构核心,负责命令的分发和执行 -- [`extensions`](../extensions/README.md) - 提供 `SendCommand()` 扩展方法 -- [`query`](../query/README.md) - 查询模式,用于数据查询 -- [`events`](../events/README.md) - 事件系统,命令执行后的通知机制 -- [`system`](../system/README.md) - 业务系统,命令的主要执行者 -- [`model`](../model/README.md) - 数据模型,命令操作的数据 +- [`architecture`](./architecture.md) - 架构核心,负责命令的分发和执行 +- [`extensions`](./extensions.md) - 提供 `SendCommand()` 扩展方法 +- [`query`](./query.md) - 查询模式,用于数据查询 +- [`events`](./events.md) - 事件系统,命令执行后的通知机制 +- [`system`](./system.md) - 业务系统,命令的主要执行者 +- [`model`](./model.md) - 数据模型,命令操作的数据 --- diff --git a/GFramework.Core.Abstractions/controller/README.md b/docs/zh-CN/core/controller.md similarity index 95% rename from GFramework.Core.Abstractions/controller/README.md rename to docs/zh-CN/core/controller.md index 7ab2fa96..a511a142 100644 --- a/GFramework.Core.Abstractions/controller/README.md +++ b/docs/zh-CN/core/controller.md @@ -399,13 +399,13 @@ public partial class GodotPlayerController : Node, IController ## 相关包 -- [`architecture`](../architecture/README.md) - 提供架构访问能力 -- [`command`](../command/README.md) - 控制器发送命令执行业务逻辑 -- [`query`](../query/README.md) - 控制器发送查询获取数据 -- [`events`](../events/README.md) - 控制器注册事件监听变化 -- [`model`](../model/README.md) - 控制器读取模型数据 -- [`system`](../system/README.md) - 控制器调用系统服务 -- [`extensions`](../extensions/README.md) - 提供便捷的扩展方法 +- [`architecture`](./architecture.md) - 提供架构访问能力 +- [`command`](./command.md) - 控制器发送命令执行业务逻辑 +- [`query`](./query.md) - 控制器发送查询获取数据 +- [`events`](./events.md) - 控制器注册事件监听变化 +- [`model`](./model.md) - 控制器读取模型数据 +- [`system`](./system.md) - 控制器调用系统服务 +- [`extensions`](./extensions.md) - 提供便捷的扩展方法 - **GFramework.Godot** - Godot 特定的控制器扩展 --- diff --git a/GFramework.Core/environment/README.md b/docs/zh-CN/core/environment.md similarity index 96% rename from GFramework.Core/environment/README.md rename to docs/zh-CN/core/environment.md index 2db0ef1e..338664cc 100644 --- a/GFramework.Core/environment/README.md +++ b/docs/zh-CN/core/environment.md @@ -213,6 +213,6 @@ public class GoodExampleSystem : AbstractSystem ## 相关包 -- [`architecture`](../architecture/README.md) - 在架构中使用环境配置 -- [`rule`](../rule/README.md) - 环境基类继承自 ContextAwareBase -- [`ioc`](../ioc/README.md) - 环境值可通过IoC容器管理 \ No newline at end of file +- [`architecture`](./architecture.md) - 在架构中使用环境配置 +- [`rule`](./rule.md) - 环境基类继承自 ContextAwareBase +- [`ioc`](./ioc.md) - 环境值可通过IoC容器管理 \ No newline at end of file diff --git a/GFramework.Core/events/README.md b/docs/zh-CN/core/events.md similarity index 96% rename from GFramework.Core/events/README.md rename to docs/zh-CN/core/events.md index 26e92072..6e4684e6 100644 --- a/GFramework.Core/events/README.md +++ b/docs/zh-CN/core/events.md @@ -516,9 +516,9 @@ public override void _Ready() ## 相关包 -- [`architecture`](../architecture/README.md) - 提供全局事件系统 -- [`extensions`](../extensions/README.md) - 提供事件扩展方法 -- [`property`](../property/README.md) - 可绑定属性基于事件实现 -- [`controller`](../controller/README.md) - 控制器监听事件 -- [`model`](../model/README.md) - 模型发送事件 -- [`system`](../system/README.md) - 系统发送和监听事件 \ No newline at end of file +- [`architecture`](./architecture.md) - 提供全局事件系统 +- [`extensions`](./extensions.md) - 提供事件扩展方法 +- [`property`](./property.md) - 可绑定属性基于事件实现 +- [`controller`](./controller.md) - 控制器监听事件 +- [`model`](./model.md) - 模型发送事件 +- [`system`](./system.md) - 系统发送和监听事件 \ No newline at end of file diff --git a/GFramework.Core/extensions/README.md b/docs/zh-CN/core/extensions.md similarity index 96% rename from GFramework.Core/extensions/README.md rename to docs/zh-CN/core/extensions.md index 3681d11e..5e3df530 100644 --- a/GFramework.Core/extensions/README.md +++ b/docs/zh-CN/core/extensions.md @@ -543,10 +543,10 @@ public class AchievementSystem : AbstractSystem ## 相关包 -- [`architecture`](../architecture/README.md) - 扩展方法最终调用架构方法 -- [`command`](../command/README.md) - 命令发送扩展 -- [`query`](../query/README.md) - 查询发送扩展 -- [`events`](../events/README.md) - 事件注册和 Or 组合扩展 -- [`model`](../model/README.md) - 模型获取扩展 -- [`system`](../system/README.md) - 系统获取扩展 -- [`utility`](../utility/README.md) - 工具获取扩展 \ No newline at end of file +- [`architecture`](./architecture.md) - 扩展方法最终调用架构方法 +- [`command`](./command.md) - 命令发送扩展 +- [`query`](./query.md) - 查询发送扩展 +- [`events`](./events.md) - 事件注册和 Or 组合扩展 +- [`model`](./model.md) - 模型获取扩展 +- [`system`](./system.md) - 系统获取扩展 +- [`utility`](./utility.md) - 工具获取扩展 \ No newline at end of file diff --git a/docs/zh-CN/core/index.md b/docs/zh-CN/core/index.md new file mode 100644 index 00000000..9f54f401 --- /dev/null +++ b/docs/zh-CN/core/index.md @@ -0,0 +1,507 @@ +# GFramework.Core 核心框架 + +> 一个基于 CQRS、MVC 和事件驱动的轻量级游戏开发架构框架 + +## 目录 + +- [框架概述](#框架概述) +- [核心概念](#核心概念) +- [架构图](#架构图) +- [快速开始](#快速开始) +- [包说明](#包说明) +- [组件联动](#组件联动) +- [最佳实践](#最佳实践) +- [设计理念](#设计理念) + +## 框架概述 + +本框架是一个与平台无关的轻量级架构,它结合了多种经典设计模式: + +- **MVC 架构模式** - 清晰的层次划分 +- **CQRS 模式** - 命令查询职责分离 +- **IoC/DI** - 依赖注入和控制反转 +- **事件驱动** - 松耦合的组件通信 +- **响应式编程** - 可绑定属性和数据流 +- **阶段式生命周期管理** - 精细化的架构状态控制 + +**重要说明**:GFramework.Core 是与平台无关的核心模块,不包含任何 Godot 特定代码。Godot 集成功能在 GFramework.Godot 包中实现。 + +### 核心特性 + +- **清晰的分层架构** - Model、View、Controller、System、Utility 各司其职 +- **类型安全** - 基于泛型的组件获取和事件系统 +- **松耦合** - 通过事件和接口实现组件解耦 +- **易于测试** - 依赖注入和纯函数设计 +- **可扩展** - 基于接口的规则体系 +- **生命周期管理** - 自动的注册和注销机制 +- **模块化** - 支持架构模块安装 +- **平台无关** - Core 模块可以在任何 .NET 环境中使用 + +## 核心概念 + +### 五层架构 + +``` +┌─────────────────────────────────────────┐ +│ View / UI │ UI 层:用户界面 +├─────────────────────────────────────────┤ +│ Controller │ 控制层:处理用户输入 +├─────────────────────────────────────────┤ +│ System │ 逻辑层:业务逻辑 +├─────────────────────────────────────────┤ +│ Model │ 数据层:游戏状态 +├─────────────────────────────────────────┤ +│ Utility │ 工具层:无状态工具 +└─────────────────────────────────────────┘ +``` + +### 横切关注点 + +``` +Command ──┐ +Query ──┼──→ 跨层操作(修改/查询数据) +Event ──┘ +``` + +### 架构阶段 + +``` +初始化:Init → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit → BeforeSystemInit → AfterSystemInit → Ready +销毁:Destroy → Destroying → Destroyed +``` + +## 架构图 + +### 整体架构 + +``` + ┌──────────────────┐ + │ Architecture │ ← 管理所有组件 + └────────┬─────────┘ + │ + ┌────────────────────┼────────────────────┐ + │ │ │ + ┌───▼────┐ ┌───▼────┐ ┌───▼─────┐ + │ Model │ │ System │ │ Utility │ + │ 层 │ │ 层 │ │ 层 │ + └───┬────┘ └───┬────┘ └────────┘ + │ │ + │ ┌─────────────┤ + │ │ │ + ┌───▼────▼───┐ ┌───▼──────┐ + │ Controller │ │ Command/ │ + │ 层 │ │ Query │ + └─────┬──────┘ └──────────┘ + │ + ┌─────▼─────┐ + │ View │ + │ UI │ + └───────────┘ +``` + +### 数据流向 + +``` +用户输入 → Controller → Command → System → Model → Event → Controller → View 更新 + +查询流程:Controller → Query → Model → 返回数据 +``` + +## 快速开始 + +本框架采用"约定优于配置"的设计理念,只需 4 步即可搭建完整的架构。 + +### 为什么需要这个框架? + +在传统开发中,我们经常遇到这些问题: + +- 代码耦合严重:UI 直接访问游戏逻辑,逻辑直接操作 UI +- 难以维护:修改一个功能需要改动多个文件 +- 难以测试:业务逻辑和 UI 混在一起无法独立测试 +- 难以复用:代码紧密耦合,无法在其他项目中复用 + +本框架通过清晰的分层解决这些问题。 + +### 1. 定义架构(Architecture) + +**作用**:Architecture 是整个应用的"中央调度器",负责管理所有组件的生命周期。 + +```csharp +using GFramework.Core.architecture; + +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册 Model - 游戏数据 + RegisterModel(new PlayerModel()); + + // 注册 System - 业务逻辑 + RegisterSystem(new CombatSystem()); + + // 注册 Utility - 工具类 + RegisterUtility(new StorageUtility()); + } +} +``` + +**优势**: + +- **依赖注入**:组件通过上下文获取架构引用 +- **集中管理**:所有组件注册在一处,一目了然 +- **生命周期管理**:自动初始化和销毁 +- **平台无关**:可以在任何 .NET 环境中使用 + +### 2. 定义 Model(数据层) + +**作用**:Model 是应用的"数据库",只负责存储和管理状态。 + +```csharp +public class PlayerModel : AbstractModel +{ + // 使用 BindableProperty 实现响应式数据 + public BindableProperty Health { get; } = new(100); + public BindableProperty Gold { get; } = new(0); + + protected override void OnInit() + { + // Model 中可以监听自己的数据变化 + Health.Register(hp => + { + if (hp <= 0) this.SendEvent(new PlayerDiedEvent()); + }); + } +} + +// 也可以不使用 BindableProperty +public class PlayerModel : AbstractModel +{ + public int Health { get; private set; } + public int Gold { get; private set; } + + protected override void OnInit() + { + Health = 100; + Gold = 0; + } +} +``` + +**优势**: + +- **数据响应式**:BindableProperty 让数据变化自动通知监听者 +- **职责单一**:只存储数据,不包含复杂业务逻辑 +- **易于测试**:可以独立测试数据逻辑 + +### 3. 定义 System(业务逻辑层) + +**作用**:System 是应用的"大脑",处理所有业务逻辑。 + +```csharp +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() + { + // System 通过事件驱动,响应游戏中的各种事件 + this.RegisterEvent(OnEnemyAttack); + } + + private void OnEnemyAttack(EnemyAttackEvent e) + { + var playerModel = this.GetModel(); + + // 处理业务逻辑:计算伤害、更新数据 + playerModel.Health.Value -= e.Damage; + + // 发送事件通知其他组件 + this.SendEvent(new PlayerTookDamageEvent { Damage = e.Damage }); + } +} +``` + +**优势**: + +- **事件驱动**:通过事件解耦,不同 System 之间松耦合 +- **可组合**:多个 System 协同工作,每个专注自己的领域 +- **易于扩展**:新增功能只需添加新的 System 和事件监听 + +### 4. 定义 Controller(控制层) + +**作用**:Controller 是"桥梁",连接 UI 和业务逻辑。 + +```csharp +public class PlayerController : IController +{ + // 通过依赖注入获取架构 + private readonly IArchitecture _architecture; + + public PlayerController(IArchitecture architecture) + { + _architecture = architecture; + } + + // 监听模型变化 + public void Initialize() + { + var playerModel = _architecture.GetModel(); + + // 数据绑定:Model 数据变化自动更新 UI + playerModel.Health.RegisterWithInitValue(OnHealthChanged); + } + + private void OnHealthChanged(int hp) + { + // 更新 UI 显示 + UpdateHealthDisplay(hp); + } + + private void UpdateHealthDisplay(int hp) { /* UI 更新逻辑 */ } +} +``` + +**优势**: + +- **自动更新 UI**:通过 BindableProperty,数据变化自动反映到界面 +- **分离关注点**:UI 逻辑和业务逻辑完全分离 +- **易于测试**:可以通过依赖注入模拟架构进行测试 + +### 完成!现在你有了一个完整的架构 + +这 4 步完成后,你就拥有了: + +- **清晰的数据层**(Model) +- **独立的业务逻辑**(System) +- **灵活的控制层**(Controller) +- **统一的生命周期管理**(Architecture) + +### 下一步该做什么? + +1. **添加 Command**:封装用户操作(如购买物品、使用技能) +2. **添加 Query**:封装数据查询(如查询背包物品数量) +3. **添加更多 System**:如任务系统、背包系统、商店系统 +4. **使用 Utility**:添加工具类(如存档工具、数学工具) +5. **使用模块**:通过 IArchitectureModule 扩展架构功能 + +## 包说明 + +| 包名 | 职责 | 文档 | +|------------------|-----------------|----------------------| +| **architecture** | 架构核心,管理所有组件生命周期 | [查看](./architecture) | +| **constants** | 框架常量定义 | 本文档 | +| **model** | 数据模型层,存储状态 | [查看](./model) | +| **system** | 业务逻辑层,处理业务规则 | [查看](./system) | +| **controller** | 控制器层,连接视图和逻辑 | (在 Abstractions 中) | +| **utility** | 工具类层,提供无状态工具 | [查看](./utility) | +| **command** | 命令模式,封装写操作 | [查看](./command) | +| **query** | 查询模式,封装读操作 | [查看](./query) | +| **events** | 事件系统,组件间通信 | [查看](./events) | +| **property** | 可绑定属性,响应式编程 | [查看](./property) | +| **ioc** | IoC 容器,依赖注入 | [查看](./ioc) | +| **rule** | 规则接口,定义组件约束 | [查看](./rule) | +| **extensions** | 扩展方法,简化 API 调用 | [查看](./extensions) | +| **logging** | 日志系统,记录运行日志 | [查看](./logging) | +| **environment** | 环境接口,提供运行环境信息 | [查看](./environment) | + +## 组件联动 + +### 1. 初始化流程 + +``` +创建 Architecture 实例 + └─> Init() + ├─> RegisterModel → Model.SetContext() → Model.Init() + ├─> RegisterSystem → System.SetContext() → System.Init() + └─> RegisterUtility → Utility 注册到容器 +``` + +### 2. Command 执行流程 + +``` +Controller.SendCommand(command) + └─> command.Execute() + └─> command.OnDo() // 子类实现 + ├─> GetModel() // 获取数据 + ├─> 修改 Model 数据 + └─> SendEvent() // 发送事件 +``` + +### 3. Event 传播流程 + +``` +组件.SendEvent(event) + └─> TypeEventSystem.Send(event) + └─> 通知所有订阅者 + ├─> Controller 响应 → 更新 UI + ├─> System 响应 → 执行逻辑 + └─> Model 响应 → 更新状态 +``` + +### 4. BindableProperty 数据绑定 + +``` +Model: BindableProperty Health = new(100); +Controller: Health.RegisterWithInitValue(hp => UpdateUI(hp)) +修改值: Health.Value = 50 → 触发所有回调 → 更新 UI +``` + +## 最佳实践 + +### 1. 分层职责原则 + +每一层都有明确的职责边界,遵循这些原则能让代码更清晰、更易维护。 + +**Model 层**: + +```csharp +// 好:只存储数据 +public class PlayerModel : AbstractModel +{ + public BindableProperty Health { get; } = new(100); + protected override void OnInit() { } +} + +// 坏:包含业务逻辑 +public class PlayerModel : AbstractModel +{ + public void TakeDamage(int damage) // 业务逻辑应在 System + { + Health.Value -= damage; + if (Health.Value <= 0) Die(); + } +} +``` + +**System 层**: + +```csharp +// 好:处理业务逻辑 +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnAttack); + } + + private void OnAttack(AttackEvent e) + { + var target = this.GetModel(); + int finalDamage = CalculateDamage(e.BaseDamage, target); + target.Health.Value -= finalDamage; + } +} +``` + +### 2. 通信方式选择指南 + +| 通信方式 | 使用场景 | 优势 | +|----------------------|-----------|----------| +| **Command** | 用户操作、修改状态 | 可撤销、可记录 | +| **Query** | 查询数据、检查条件 | 明确只读意图 | +| **Event** | 通知其他组件 | 松耦合、可扩展 | +| **BindableProperty** | 数据变化通知 | 自动化、不会遗漏 | + +### 3. 生命周期管理 + +**为什么需要注销?** + +忘记注销监听器会导致: + +- **内存泄漏**:对象无法被 GC 回收 +- **逻辑错误**:已销毁的对象仍在响应事件 + +```csharp +// 使用 UnRegisterList 统一管理 +private IUnRegisterList _unregisterList = new UnRegisterList(); + +public void Initialize() +{ + this.RegisterEvent(OnEvent1) + .AddToUnregisterList(_unregisterList); + + model.Property.Register(OnPropertyChanged) + .AddToUnregisterList(_unregisterList); +} + +public void Cleanup() +{ + _unregisterList.UnRegisterAll(); +} +``` + +### 4. 性能优化技巧 + +```csharp +// 低效:每帧都查询 +var model = _architecture.GetModel(); // 频繁调用 + +// 高效:缓存引用 +private PlayerModel _playerModel; + +public void Initialize() +{ + _playerModel = _architecture.GetModel(); // 只查询一次 +} +``` + +## 设计理念 + +框架的设计遵循 SOLID 原则和经典设计模式。 + +### 1. 单一职责原则(SRP) + +- **Model**:只负责存储数据 +- **System**:只负责处理业务逻辑 +- **Controller**:只负责协调和输入处理 +- **Utility**:只负责提供工具方法 + +### 2. 开闭原则(OCP) + +- 通过**事件系统**添加新功能,无需修改现有代码 +- 新的 System 可以监听现有事件,插入自己的逻辑 + +### 3. 依赖倒置原则(DIP) + +- 所有组件通过接口交互 +- 通过 IoC 容器注入依赖 +- 易于替换实现和编写测试 + +### 4. 接口隔离原则(ISP) + +```csharp +// 小而专注的接口 +public interface ICanGetModel : IBelongToArchitecture { } +public interface ICanSendCommand : IBelongToArchitecture { } +public interface ICanRegisterEvent : IBelongToArchitecture { } + +// 组合需要的能力 +public interface IController : + ICanGetModel, + ICanSendCommand, + ICanRegisterEvent { } +``` + +### 5. 组合优于继承 + +通过接口组合获得能力,而不是通过继承。 + +### 框架核心设计模式 + +| 设计模式 | 应用位置 | 解决的问题 | 带来的好处 | +|-----------|------------|----------|--------| +| **工厂模式** | IoC 容器 | 组件的创建和管理 | 解耦创建逻辑 | +| **观察者模式** | Event 系统 | 组件间的通信 | 松耦合通信 | +| **命令模式** | Command | 封装操作请求 | 支持撤销重做 | +| **策略模式** | System | 不同的业务逻辑 | 易于切换策略 | +| **依赖注入** | 整体架构 | 组件间的依赖 | 自动管理依赖 | +| **模板方法** | Abstract 类 | 定义算法骨架 | 统一流程规范 | + +### 平台无关性 + +- **GFramework.Core**:纯 .NET 库,无任何平台特定代码 +- **GFramework.Godot**:Godot 特定实现,包含 Node 扩展、GodotLogger 等 +- 可以轻松将 Core 框架移植到其他平台(Unity、.NET MAUI 等) + +--- + +**版本**: 1.0.0 +**许可证**: Apache 2.0 \ No newline at end of file diff --git a/GFramework.Core/ioc/README.md b/docs/zh-CN/core/ioc.md similarity index 98% rename from GFramework.Core/ioc/README.md rename to docs/zh-CN/core/ioc.md index 9314e6f7..9b15b7dd 100644 --- a/GFramework.Core/ioc/README.md +++ b/docs/zh-CN/core/ioc.md @@ -683,7 +683,7 @@ protected override void OnInit() ## 相关包 -- [`architecture`](../architecture/README.md) - 使用 IoC 容器管理所有组件 -- [`model`](../model/README.md) - Model 通过 IoC 容器注册和获取 -- [`system`](../system/README.md) - System 通过 IoC 容器注册和获取 -- [`utility`](../utility/README.md) - Utility 通过 IoC 容器注册和获取 \ No newline at end of file +- [`architecture`](./architecture.md) - 使用 IoC 容器管理所有组件 +- [`model`](./model.md) - Model 通过 IoC 容器注册和获取 +- [`system`](./system.md) - System 通过 IoC 容器注册和获取 +- [`utility`](./utility.md) - Utility 通过 IoC 容器注册和获取 \ No newline at end of file diff --git a/GFramework.Core/logging/README.md b/docs/zh-CN/core/logging.md similarity index 97% rename from GFramework.Core/logging/README.md rename to docs/zh-CN/core/logging.md index 6f93aac4..4a6ecdf8 100644 --- a/GFramework.Core/logging/README.md +++ b/docs/zh-CN/core/logging.md @@ -355,9 +355,9 @@ public class DebugLogger : AbstractLogger ## 相关包 -- [architecture](../architecture/README.md) - 架构核心,使用日志系统记录生命周期事件 -- [property](../property/README.md) - 可绑定属性基于事件系统实现 -- [extensions](../extensions/README.md) - 提供便捷的扩展方法 +- [architecture](./architecture.md) - 架构核心,使用日志系统记录生命周期事件 +- [property](./property.md) - 可绑定属性基于事件系统实现 +- [extensions](./extensions.md) - 提供便捷的扩展方法 --- diff --git a/GFramework.Core/model/README.md b/docs/zh-CN/core/model.md similarity index 92% rename from GFramework.Core/model/README.md rename to docs/zh-CN/core/model.md index df290493..ddf6f804 100644 --- a/GFramework.Core/model/README.md +++ b/docs/zh-CN/core/model.md @@ -177,8 +177,8 @@ public class GameModel : AbstractModel ## 相关包 -- [`architecture`](../architecture/README.md) - 提供 Model 的注册和获取 -- [`property`](../property/README.md) - BindableProperty 用于定义可监听属性 -- [`events`](../events/README.md) - Model 发送事件通知变化 -- [`utility`](../utility/README.md) - Model 可以使用工具类 -- [`extensions`](../extensions/README.md) - 提供 GetModel 等扩展方法 \ No newline at end of file +- [`architecture`](./architecture.md) - 提供 Model 的注册和获取 +- [`property`](./property.md) - BindableProperty 用于定义可监听属性 +- [`events`](./events.md) - Model 发送事件通知变化 +- [`utility`](./utility.md) - Model 可以使用工具类 +- [`extensions`](./extensions.md) - 提供 GetModel 等扩展方法 \ No newline at end of file diff --git a/GFramework.Core/pool/README.md b/docs/zh-CN/core/pool.md similarity index 99% rename from GFramework.Core/pool/README.md rename to docs/zh-CN/core/pool.md index f2f690f0..f818dfa0 100644 --- a/GFramework.Core/pool/README.md +++ b/docs/zh-CN/core/pool.md @@ -943,8 +943,8 @@ public class BatchPoolSystem : AbstractObjectPoolSystem ## 相关包 -- [`system`](../system/README.md) - 对象池系统继承自 AbstractSystem -- [`architecture`](../architecture/README.md) - 在架构中注册对象池系统 +- [`system`](./system.md) - 对象池系统继承自 AbstractSystem +- [`architecture`](./architecture.md) - 在架构中注册对象池系统 --- diff --git a/GFramework.Core/property/README.md b/docs/zh-CN/core/property.md similarity index 97% rename from GFramework.Core/property/README.md rename to docs/zh-CN/core/property.md index 2f899078..464d4a6c 100644 --- a/GFramework.Core/property/README.md +++ b/docs/zh-CN/core/property.md @@ -332,9 +332,9 @@ _mOnValueChanged?.Invoke(value); ## 相关包 -- [`model`](../model/README.md) - Model 中大量使用 BindableProperty -- [`events`](../events/README.md) - BindableProperty 基于事件系统实现 -- [`extensions`](../extensions/README.md) - 提供便捷的注销扩展方法 +- [`model`](./model.md) - Model 中大量使用 BindableProperty +- [`events`](./events.md) - BindableProperty 基于事件系统实现 +- [`extensions`](./extensions.md) - 提供便捷的注销扩展方法 --- diff --git a/GFramework.Core/query/README.md b/docs/zh-CN/core/query.md similarity index 96% rename from GFramework.Core/query/README.md rename to docs/zh-CN/core/query.md index e8947c34..c0556fee 100644 --- a/GFramework.Core/query/README.md +++ b/docs/zh-CN/core/query.md @@ -440,8 +440,8 @@ public class GetMultipleItemCountsQuery : AbstractQuery> ## 相关包 -- [`command`](../command/README.md) - CQRS 的命令部分 -- [`model`](../model/README.md) - Query 主要从 Model 获取数据 -- [`system`](../system/README.md) - System 中可以发送 Query -- [`controller`](../controller/README.md) - Controller 中可以发送 Query -- [`extensions`](../extensions/README.md) - 提供 SendQuery 扩展方法 \ No newline at end of file +- [`command`](./command.md) - CQRS 的命令部分 +- [`model`](./model.md) - Query 主要从 Model 获取数据 +- [`system`](./system.md) - System 中可以发送 Query +- [`controller`](./controller.md) - Controller 中可以发送 Query +- [`extensions`](./extensions.md) - 提供 SendQuery 扩展方法 \ No newline at end of file diff --git a/GFramework.Core/rule/README.md b/docs/zh-CN/core/rule.md similarity index 93% rename from GFramework.Core/rule/README.md rename to docs/zh-CN/core/rule.md index 6614df33..87eb4453 100644 --- a/GFramework.Core/rule/README.md +++ b/docs/zh-CN/core/rule.md @@ -307,10 +307,10 @@ public class DatabaseCommand : AbstractCommand, ICanAccessDatabase ## 相关包 -- [`architecture`](../architecture/README.md) - 定义 IArchitectureContext 接口 -- [`command`](../command/README.md) - Command 继承 AbstractCommand (IContextAware) -- [`query`](../query/README.md) - Query 继承 AbstractQuery (IContextAware) -- [`controller`](../controller/README.md) - Controller 实现 ICanSendCommand 等接口 -- [`system`](../system/README.md) - System 继承 AbstractSystem (IContextAware) -- [`model`](../model/README.md) - Model 继承 AbstractModel (IContextAware) -- [`extensions`](../extensions/README.md) - 基于规则接口提供扩展方法 \ No newline at end of file +- [`architecture`](./architecture.md) - 定义 IArchitectureContext 接口 +- [`command`](./command.md) - Command 继承 AbstractCommand (IContextAware) +- [`query`](./query.md) - Query 继承 AbstractQuery (IContextAware) +- [`controller`](./controller.md) - Controller 实现 ICanSendCommand 等接口 +- [`system`](./system.md) - System 继承 AbstractSystem (IContextAware) +- [`model`](./model.md) - Model 继承 AbstractModel (IContextAware) +- [`extensions`](./extensions.md) - 基于规则接口提供扩展方法 \ No newline at end of file diff --git a/GFramework.Core/system/README.md b/docs/zh-CN/core/system.md similarity index 97% rename from GFramework.Core/system/README.md rename to docs/zh-CN/core/system.md index 916a1dc0..cb8f421a 100644 --- a/GFramework.Core/system/README.md +++ b/docs/zh-CN/core/system.md @@ -536,9 +536,9 @@ public class ParticleSystem : AbstractSystem ## 相关包 -- [`model`](../model/README.md) - System 操作 Model 的数据 -- [`events`](../events/README.md) - System 通过事件通信 -- [`command`](../command/README.md) - System 中可以发送 Command -- [`query`](../query/README.md) - System 中可以发送 Query -- [`utility`](../utility/README.md) - System 可以使用 Utility -- [`architecture`](../architecture/README.md) - 在架构中注册 System \ No newline at end of file +- [`model`](./model.md) - System 操作 Model 的数据 +- [`events`](./events.md) - System 通过事件通信 +- [`command`](./command.md) - System 中可以发送 Command +- [`query`](./query.md) - System 中可以发送 Query +- [`utility`](./utility.md) - System 可以使用 Utility +- [`architecture`](./architecture.md) - 在架构中注册 System \ No newline at end of file diff --git a/GFramework.Core/utility/README.md b/docs/zh-CN/core/utility.md similarity index 97% rename from GFramework.Core/utility/README.md rename to docs/zh-CN/core/utility.md index 1bc5924a..711484c3 100644 --- a/GFramework.Core/utility/README.md +++ b/docs/zh-CN/core/utility.md @@ -606,8 +606,8 @@ public class CollectionUtility : IUtility ## 相关包 -- [`system`](../system/README.md) - System 中使用 Utility -- [`command`](../command/README.md) - Command 中可以使用 Utility -- [`architecture`](../architecture/README.md) - 在架构中注册 Utility -- [`ioc`](../ioc/README.md) - Utility 通过 IoC 容器管理 -- [`extensions`](../extensions/README.md) - 提供 GetUtility 扩展方法 \ No newline at end of file +- [`system`](./system.md) - System 中使用 Utility +- [`command`](./command.md) - Command 中可以使用 Utility +- [`architecture`](./architecture.md) - 在架构中注册 Utility +- [`ioc`](./ioc.md) - Utility 通过 IoC 容器管理 +- [`extensions`](./extensions.md) - 提供 GetUtility 扩展方法 \ No newline at end of file diff --git a/docs/zh-CN/game/index.md b/docs/zh-CN/game/index.md new file mode 100644 index 00000000..7bf506ac --- /dev/null +++ b/docs/zh-CN/game/index.md @@ -0,0 +1,1406 @@ +# GFramework.Game + +> 游戏特定功能抽象 - 为游戏开发提供专门的工具和系统 + +GFramework.Game 是 GFramework 框架的游戏特定功能模块,提供了游戏开发中常用的抽象和工具,包括资产管理、存储系统、序列化等核心功能。 + +## 📋 目录 + +- [概述](#概述) +- [核心特性](#核心特性) +- [架构模块系统](#架构模块系统) +- [资产管理](#资产管理) +- [存储系统](#存储系统) +- [序列化系统](#序列化系统) +- [使用示例](#使用示例) +- [最佳实践](#最佳实践) +- [性能特性](#性能特性) + +## 概述 + +GFramework.Game 为游戏开发提供了专门的功能模块,与 GFramework.Core 的平台无关特性完美结合,为游戏项目提供了一整套完整的解决方案。 + +### 核心设计理念 + +- **游戏导向**:专门针对游戏开发场景设计 +- **模块化架构**:可插拔的模块系统,按需组合 +- **数据持久化**:完善的存档和数据管理方案 +- **资源管理**:高效的资源加载和管理机制 + +## 核心特性 + +### 🏗️ 模块化架构 + +- **AbstractModule**:可重用的架构模块基类 +- **生命周期管理**:与框架生命周期深度集成 +- **依赖注入**:模块间的依赖自动管理 +- **配置驱动**:灵活的模块配置系统 + +### 📦 资产管理 + +- **统一资源目录**:集中化的资源注册和查询 +- **类型安全**:编译时类型检查和泛型支持 +- **重复检测**:自动检测资源重复注册 +- **映射支持**:灵活的资源映射和别名系统 + +### 💾 存储系统 + +- **分层存储**:命名空间支持的存储隔离 +- **多格式支持**:JSON、二进制等多种存储格式 +- **异步操作**:完整的异步存储 API +- **版本兼容**:存档版本管理和迁移支持 + +### 🔄 序列化系统 + +- **JSON 集成**:基于 Newtonsoft.Json 的序列化 +- **自定义序列化**:支持自定义序列化逻辑 +- **性能优化**:序列化缓存和优化策略 +- **类型安全**:强类型的序列化和反序列化 + +## 架构模块系统 + +### AbstractModule 基础使用 + +```csharp +using GFramework.Game.architecture; + +public class AudioModule : AbstractModule +{ + public override void Install(IArchitecture architecture) + { + // 注册音频相关系统 + architecture.RegisterSystem(new AudioSystem()); + architecture.RegisterSystem(new MusicSystem()); + + // 注册音频工具 + architecture.RegisterUtility(new AudioUtility()); + architecture.RegisterUtility(new MusicUtility()); + } + + public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) + { + switch (phase) + { + case ArchitecturePhase.BeforeModelInit: + // 在模型初始化前准备音频资源 + PreloadAudioResources(); + break; + + case ArchitecturePhase.Ready: + // 架构准备就绪,开始播放背景音乐 + StartBackgroundMusic(); + break; + + case ArchitecturePhase.Destroying: + // 清理音频资源 + CleanupAudioResources(); + break; + } + } + + private void PreloadAudioResources() + { + // 预加载音频资源 + var audioUtility = architecture.GetUtility(); + audioUtility.PreloadAudio("background_music"); + audioUtility.PreloadAudio("shoot_sound"); + audioUtility.PreloadAudio("explosion_sound"); + } + + private void StartBackgroundMusic() + { + var musicSystem = architecture.GetSystem(); + musicSystem.PlayBackgroundMusic("background_music"); + } + + private void CleanupAudioResources() + { + var audioUtility = architecture.GetUtility(); + audioUtility.UnloadAllAudio(); + } +} +``` + +### 复杂模块示例 + +```csharp +public class SaveModule : AbstractModule +{ + private ISaveSystem _saveSystem; + private IDataMigrationManager _migrationManager; + + public override void Install(IArchitecture architecture) + { + // 注册存储相关组件 + _saveSystem = new SaveSystem(); + architecture.RegisterUtility(_saveSystem); + + _migrationManager = new DataMigrationManager(); + architecture.RegisterUtility(_migrationManager); + + // 注册数据版本管理 + architecture.RegisterSystem(new SaveDataVersionSystem()); + } + + public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) + { + switch (phase) + { + case ArchitecturePhase.AfterModelInit: + // 在模型初始化后设置数据迁移 + SetupDataMigrations(); + break; + + case ArchitecturePhase.Ready: + // 尝试自动加载存档 + TryAutoLoadSave(); + break; + } + } + + private void SetupDataMigrations() + { + // 设置数据版本迁移 + _migrationManager.RegisterMigration(1, 2, MigratePlayerDataV1ToV2); + _migrationManager.RegisterMigration(2, 3, MigratePlayerDataV2ToV3); + } + + private void TryAutoLoadSave() + { + if (_saveSystem.HasAutoSave()) + { + _saveSystem.LoadAutoSave(); + } + } + + private PlayerData MigratePlayerDataV1ToV2(PlayerData v1Data) + { + return new PlayerData + { + // 迁移逻辑 + Name = v1Data.Name, + Health = v1Data.Health, + // 新增字段 + MaxHealth = 100, + Level = 1 + }; + } + + private PlayerData MigratePlayerDataV2ToV3(PlayerData v2Data) + { + return new PlayerData + { + // 迁移逻辑 + Name = v2Data.Name, + Health = v2Data.Health, + MaxHealth = v2Data.MaxHealth, + Level = v2Data.Level, + // 新增字段 + Experience = 0, + Skills = new List() + }; + } +} +``` + +### 模块配置 + +```csharp +public class ModuleConfig +{ + public string SaveDirectory { get; set; } = "saves"; + public int AutoSaveInterval { get; set; } = 300; // 5分钟 + public bool EnableDataCompression { get; set; } = true; + public int MaxSaveSlots { get; set; } = 10; +} + +public class ConfigurableSaveModule : AbstractModule +{ + private ModuleConfig _config; + + public ConfigurableSaveModule(ModuleConfig config) + { + _config = config; + } + + public override void Install(IArchitecture architecture) + { + var saveSystem = new SaveSystem(_config); + architecture.RegisterUtility(saveSystem); + + // 配置自动保存 + if (_config.AutoSaveInterval > 0) + { + architecture.RegisterSystem(new AutoSaveSystem(_config.AutoSaveInterval)); + } + } +} +``` + +## 资产管理 + +### AbstractAssetCatalogUtility 基础使用 + +```csharp +using GFramework.Game.assets; + +public class GameAssetCatalog : AbstractAssetCatalogUtility +{ + public override void Initialize() + { + base.Initialize(); + + // 注册场景资产 + RegisterSceneUnit("Player", "res://scenes/Player.tscn"); + RegisterSceneUnit("Enemy", "res://scenes/Enemy.tscn"); + RegisterSceneUnit("Bullet", "res://scenes/Bullet.tscn"); + + // 注册场景页面 + RegisterScenePage("MainMenu", "res://ui/MainMenu.tscn"); + RegisterScenePage("GameUI", "res://ui/GameUI.tscn"); + RegisterScenePage("PauseMenu", "res://ui/PauseMenu.tscn"); + + // 注册通用资产 + RegisterAsset("PlayerTexture", "res://textures/player.png"); + RegisterAsset("EnemyTexture", "res://textures/enemy.png"); + RegisterAsset("ShootSound", "res://audio/shoot.wav"); + RegisterAsset("ExplosionSound", "res://audio/explosion.wav"); + } + + // 自定义资产验证 + protected override bool ValidateAsset(string key, string path) + { + if (!FileAccess.FileExists(path)) + { + GD.PrintErr($"Asset file not found: {path}"); + return false; + } + + return true; + } + + // 资产加载完成回调 + protected override void OnAssetLoaded(string key, object asset) + { + GD.Print($"Asset loaded: {key}"); + + // 对特定资产进行额外处理 + if (key == "PlayerTexture") + { + var texture = (Texture2D)asset; + // 预处理纹理... + } + } +} +``` + +### 资产映射系统 + +```csharp +public class AssetMapping +{ + public string Key { get; set; } + public string Path { get; set; } + public Type Type { get; set; } + public Dictionary Metadata { get; set; } = new(); +} + +public class AdvancedAssetCatalog : AbstractAssetCatalogUtility +{ + public override void Initialize() + { + base.Initialize(); + + // 使用映射对象注册资产 + RegisterAsset(new AssetMapping + { + Key = "Player", + Path = "res://scenes/Player.tscn", + Type = typeof(PackedScene), + Metadata = new Dictionary + { + ["category"] = "character", + ["tags"] = new[] { "player", "hero", "controlled" }, + ["health"] = 100, + ["speed"] = 5.0f + } + }); + + // 批量注册 + RegisterAssetsFromDirectory("res://textures/", "*.png", "texture"); + RegisterAssetsFromDirectory("res://audio/", "*.wav", "sound"); + } + + private void RegisterAssetsFromDirectory(string directory, string pattern, string prefix) + { + var dir = DirAccess.Open(directory); + if (dir == null) return; + + dir.ListDirBegin(); + var fileName = dir.GetNext(); + + while (!string.IsNullOrEmpty(fileName)) + { + if (fileName.EndsWith(pattern.Substring(1))) + { + var key = $"{prefix}{Path.GetFileNameWithoutExtension(fileName)}"; + var path = Path.Combine(directory, fileName); + + RegisterAsset(key, path); + } + + fileName = dir.GetNext(); + } + + dir.ListDirEnd(); + } +} +``` + +### 资产工厂模式 + +```csharp +public interface IAssetFactory +{ + T Create(string key); + bool CanCreate(string key); +} + +public class PlayerFactory : IAssetFactory +{ + private readonly AbstractAssetCatalogUtility _catalog; + + public PlayerFactory(AbstractAssetCatalogUtility catalog) + { + _catalog = catalog; + } + + public Player Create(string key) + { + var scene = _catalog.GetScene(key); + var player = scene.Instantiate(); + + // 配置玩家 + player.Health = GetPlayerHealth(key); + player.Speed = GetPlayerSpeed(key); + + return player; + } + + public bool CanCreate(string key) + { + return _catalog.HasScene(key) && key.StartsWith("Player"); + } + + private int GetPlayerHealth(string key) + { + var metadata = _catalog.GetAssetMetadata(key); + return metadata?.GetValueOrDefault("health", 100) ?? 100; + } + + private float GetPlayerSpeed(string key) + { + var metadata = _catalog.GetAssetMetadata(key); + return metadata?.GetValueOrDefault("speed", 5.0f) ?? 5.0f; + } +} +``` + +## 存储系统 + +### ScopedStorage 分层存储 + +```csharp +using GFramework.Game.storage; + +public class GameDataManager +{ + private readonly IStorage _rootStorage; + private readonly IStorage _playerStorage; + private readonly IStorage _saveStorage; + + public GameDataManager(IStorage rootStorage) + { + _rootStorage = rootStorage; + + // 创建分层存储 + _playerStorage = new ScopedStorage(rootStorage, "player"); + _saveStorage = new ScopedStorage(rootStorage, "saves"); + } + + public void SavePlayerData(string playerId, PlayerData data) + { + _playerStorage.Write($"{playerId}/profile", data); + _playerStorage.Write($"{playerId}/inventory", data.Inventory); + _playerStorage.Write($"{playerId}/stats", data.Stats); + } + + public PlayerData LoadPlayerData(string playerId) + { + var profile = _playerStorage.Read($"{playerId}/profile"); + var inventory = _playerStorage.Read($"{playerId}/inventory", new Inventory()); + var stats = _playerStorage.Read($"{playerId}/stats", new PlayerStats()); + + return new PlayerData + { + Profile = profile, + Inventory = inventory, + Stats = stats + }; + } + + public void SaveGame(int slotId, SaveData data) + { + _saveStorage.Write($"slot_{slotId}", data); + _saveStorage.Write($"slot_{slotId}/timestamp", DateTime.Now); + _saveStorage.Write($"slot_{slotId}/version", data.Version); + } + + public SaveData LoadGame(int slotId) + { + return _saveStorage.Read($"slot_{slotId}"); + } + + public List GetSaveSlotInfos() + { + var infos = new List(); + + for (int i = 1; i <= 10; i++) + { + var timestamp = _saveStorage.Read($"slot_{i}/timestamp"); + var version = _saveStorage.Read($"slot_{i}/version"); + + if (timestamp != default) + { + infos.Add(new SaveSlotInfo + { + SlotId = i, + Timestamp = timestamp, + Version = version + }); + } + } + + return infos; + } +} +``` + +### 自定义存储实现 + +```csharp +public class EncryptedStorage : IStorage +{ + private readonly IStorage _innerStorage; + private readonly IEncryptor _encryptor; + + public EncryptedStorage(IStorage innerStorage, IEncryptor encryptor) + { + _innerStorage = innerStorage; + _encryptor = encryptor; + } + + public void Write(string key, T data) + { + var json = JsonConvert.SerializeObject(data); + var encrypted = _encryptor.Encrypt(json); + _innerStorage.Write(key, encrypted); + } + + public T Read(string key, T defaultValue = default) + { + var encrypted = _innerStorage.Read(key); + if (string.IsNullOrEmpty(encrypted)) + return defaultValue; + + var json = _encryptor.Decrypt(encrypted); + return JsonConvert.DeserializeObject(json); + } + + public async Task WriteAsync(string key, T data) + { + var json = JsonConvert.SerializeObject(data); + var encrypted = _encryptor.Encrypt(json); + await _innerStorage.WriteAsync(key, encrypted); + } + + public async Task ReadAsync(string key, T defaultValue = default) + { + var encrypted = await _innerStorage.ReadAsync(key); + if (string.IsNullOrEmpty(encrypted)) + return defaultValue; + + var json = _encryptor.Decrypt(encrypted); + return JsonConvert.DeserializeObject(json); + } + + public bool Has(string key) + { + return _innerStorage.Has(key); + } + + public void Delete(string key) + { + _innerStorage.Delete(key); + } + + public void Clear() + { + _innerStorage.Clear(); + } +} +``` + +### 存储缓存层 + +```csharp +public class CachedStorage : IStorage +{ + private readonly IStorage _innerStorage; + private readonly Dictionary _cache; + private readonly Dictionary _cacheTimestamps; + private readonly TimeSpan _cacheExpiry; + + public CachedStorage(IStorage innerStorage, TimeSpan cacheExpiry = default) + { + _innerStorage = innerStorage; + _cacheExpiry = cacheExpiry == default ? TimeSpan.FromMinutes(5) : cacheExpiry; + _cache = new Dictionary(); + _cacheTimestamps = new Dictionary(); + } + + public T Read(string key, T defaultValue = default) + { + if (_cache.TryGetValue(key, out var cachedValue) && + !IsCacheExpired(key)) + { + return (T)cachedValue; + } + + var value = _innerStorage.Read(key, defaultValue); + UpdateCache(key, value); + + return value; + } + + public void Write(string key, T data) + { + _innerStorage.Write(key, data); + UpdateCache(key, data); + } + + public async Task ReadAsync(string key, T defaultValue = default) + { + if (_cache.TryGetValue(key, out var cachedValue) && + !IsCacheExpired(key)) + { + return (T)cachedValue; + } + + var value = await _innerStorage.ReadAsync(key, defaultValue); + UpdateCache(key, value); + + return value; + } + + public async Task WriteAsync(string key, T data) + { + await _innerStorage.WriteAsync(key, data); + UpdateCache(key, data); + } + + public bool Has(string key) + { + return _cache.ContainsKey(key) || _innerStorage.Has(key); + } + + public void Delete(string key) + { + _cache.Remove(key); + _cacheTimestamps.Remove(key); + _innerStorage.Delete(key); + } + + public void Clear() + { + _cache.Clear(); + _cacheTimestamps.Clear(); + _innerStorage.Clear(); + } + + public void ClearCache() + { + _cache.Clear(); + _cacheTimestamps.Clear(); + } + + private bool IsCacheExpired(string key) + { + if (!_cacheTimestamps.TryGetValue(key, out var timestamp)) + return true; + + return DateTime.Now - timestamp > _cacheExpiry; + } + + private void UpdateCache(string key, T value) + { + _cache[key] = value; + _cacheTimestamps[key] = DateTime.Now; + } +} +``` + +## 序列化系统 + +### JsonSerializer 使用 + +```csharp +using GFramework.Game.serializer; + +public class GameDataSerializer +{ + private readonly JsonSerializer _serializer; + + public GameDataSerializer() + { + _serializer = new JsonSerializer(new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Populate, + TypeNameHandling = TypeNameHandling.Auto + }); + + // 自定义转换器 + _serializer.Converters.Add(new Vector2JsonConverter()); + _serializer.Converters.Add(new ColorJsonConverter()); + _serializer.Converters.Add(new GodotResourceJsonConverter()); + } + + public string Serialize(T data) + { + return _serializer.Serialize(data); + } + + public T Deserialize(string json) + { + return _serializer.Deserialize(json); + } + + public void SerializeToFile(string path, T data) + { + var json = Serialize(data); + FileAccess.Open(path, FileAccess.ModeFlags.Write).StoreString(json); + } + + public T DeserializeFromFile(string path) + { + if (!FileAccess.FileExists(path)) + return default(T); + + var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); + var json = file.GetAsText(); + file.Close(); + + return Deserialize(json); + } +} +``` + +### 自定义 JSON 转换器 + +```csharp +public class Vector2JsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("x"); + writer.WriteValue(value.X); + writer.WritePropertyName("y"); + writer.WriteValue(value.Y); + writer.WriteEndObject(); + } + + public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return Vector2.Zero; + + var obj = serializer.Deserialize>(reader); + return new Vector2(obj["x"], obj["y"]); + } +} + +public class ColorJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer) + { + writer.WriteValue(value.ToHtml()); + } + + public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return Colors.White; + + var html = reader.Value.ToString(); + return Color.FromHtml(html); + } +} + +public class GodotResourceJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, Resource value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + writer.WriteStartObject(); + writer.WritePropertyName("$type"); + writer.WriteValue(value.GetType().Name); + writer.WritePropertyName("path"); + writer.WriteValue(value.ResourcePath); + writer.WriteEndObject(); + } + + public override Resource ReadJson(JsonReader reader, Type objectType, Resource existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + var obj = serializer.Deserialize>(reader); + var path = obj["path"]; + + return GD.Load(path); + } +} +``` + +### 版本化序列化 + +```csharp +public class VersionedData +{ + public int Version { get; set; } + public Dictionary Data { get; set; } = new(); +} + +public class VersionedSerializer +{ + private readonly Dictionary _typeVersions = new(); + private readonly Dictionary _migrations = new(); + + public void RegisterVersion(int version) + { + _typeVersions[typeof(T)] = version; + } + + public void RegisterMigration(int fromVersion, int toVersion, IDataMigration migration) + { + var key = GetMigrationKey(typeof(T), fromVersion, toVersion); + _migrations[key] = migration; + } + + public string Serialize(T data) + { + var versioned = new VersionedData + { + Version = _typeVersions.GetValueOrDefault(typeof(T), 1), + Data = new Dictionary + { + ["type"] = typeof(T).Name, + ["data"] = JsonConvert.SerializeObject(data) + } + }; + + return JsonConvert.SerializeObject(versioned); + } + + public T Deserialize(string json) + { + var versioned = JsonConvert.DeserializeObject(json); + var currentVersion = _typeVersions.GetValueOrDefault(typeof(T), 1); + + // 迁移数据到当前版本 + var dataJson = MigrateData(versioned.Data["data"] as string, versioned.Version, currentVersion); + + return JsonConvert.DeserializeObject(dataJson); + } + + private string MigrateData(string dataJson, int fromVersion, int toVersion) + { + var currentData = dataJson; + var currentVersion = fromVersion; + + while (currentVersion < toVersion) + { + var migrationKey = GetMigrationKey(typeof(T), currentVersion, currentVersion + 1); + + if (_migrations.TryGetValue(migrationKey, out var migration)) + { + currentData = migration.Migrate(currentData); + currentVersion++; + } + else + { + throw new InvalidOperationException($"No migration found from version {currentVersion} to {currentVersion + 1}"); + } + } + + return currentData; + } + + private int GetMigrationKey(Type type, int fromVersion, int toVersion) + { + return HashCode.Combine(type.Name, fromVersion, toVersion); + } +} + +public interface IDataMigration +{ + string Migrate(string data); +} + +public interface IDataMigration : IDataMigration +{ + T Migrate(T data); +} +``` + +## 使用示例 + +### 完整的游戏数据管理系统 + +```csharp +// 1. 定义数据模型 +public class GameProfile +{ + public string PlayerName { get; set; } + public DateTime LastPlayed { get; set; } + public int TotalPlayTime { get; set; } + public List UnlockedAchievements { get; set; } = new(); +} + +public class GameSettings +{ + public float MasterVolume { get; set; } = 1.0f; + public float MusicVolume { get; set; } = 0.8f; + public float SFXVolume { get; set; } = 0.9f; + public GraphicsSettings Graphics { get; set; } = new(); + public InputSettings Input { get; set; } = new(); +} + +// 2. 创建游戏管理器 +[ContextAware] +[Log] +public partial class GameManager : Node, IController +{ + private GameAssetCatalog _assetCatalog; + private GameDataManager _dataManager; + private GameDataSerializer _serializer; + + protected override void OnInit() + { + // 初始化资产目录 + _assetCatalog = new GameAssetCatalog(); + _assetCatalog.Initialize(); + + // 初始化存储系统 + var rootStorage = new FileStorage("user://data/"); + var cachedStorage = new CachedStorage(rootStorage); + _dataManager = new GameDataManager(cachedStorage); + + // 初始化序列化器 + _serializer = new GameDataSerializer(); + + Logger.Info("Game manager initialized"); + } + + public void StartNewGame(string playerName) + { + Logger.Info($"Starting new game for player: {playerName}"); + + // 创建新的游戏档案 + var profile = new GameProfile + { + PlayerName = playerName, + LastPlayed = DateTime.Now, + TotalPlayTime = 0 + }; + + _dataManager.SavePlayerData("current", profile); + + // 加载初始场景 + LoadInitialScene(); + + Context.SendEvent(new NewGameStartedEvent { PlayerName = playerName }); + } + + public void LoadGame(int slotId) + { + Logger.Info($"Loading game from slot {slotId}"); + + try + { + var saveData = _dataManager.LoadGame(slotId); + if (saveData != null) + { + // 恢复游戏状态 + RestoreGameState(saveData); + + Context.SendEvent(new GameLoadedEvent { SlotId = slotId }); + Logger.Info("Game loaded successfully"); + } + else + { + Logger.Warning($"No save data found in slot {slotId}"); + Context.SendEvent(new GameLoadFailedEvent { SlotId = slotId }); + } + } + catch (Exception ex) + { + Logger.Error($"Failed to load game: {ex.Message}"); + Context.SendEvent(new GameLoadFailedEvent { SlotId = slotId, Error = ex.Message }); + } + } + + public void SaveGame(int slotId) + { + Logger.Info($"Saving game to slot {slotId}"); + + try + { + var saveData = CreateSaveData(); + _dataManager.SaveGame(slotId, saveData); + + Context.SendEvent(new GameSavedEvent { SlotId = slotId }); + Logger.Info("Game saved successfully"); + } + catch (Exception ex) + { + Logger.Error($"Failed to save game: {ex.Message}"); + Context.SendEvent(new GameSaveFailedEvent { SlotId = slotId, Error = ex.Message }); + } + } + + private void LoadInitialScene() + { + var playerScene = _assetCatalog.GetScene("Player"); + var player = playerScene.Instantiate(); + + var gameWorldScene = _assetCatalog.GetScene("GameWorld"); + var gameWorld = gameWorldScene.Instantiate(); + + AddChild(gameWorld); + gameWorld.AddChild(player); + } + + private void RestoreGameState(SaveData saveData) + { + // 恢复玩家位置 + var playerScene = _assetCatalog.GetScene("Player"); + var player = playerScene.Instantiate(); + player.Position = saveData.PlayerPosition; + + // 恢复游戏世界 + var gameWorldScene = _assetCatalog.GetScene("GameWorld"); + var gameWorld = gameWorldScene.Instantiate(); + + AddChild(gameWorld); + gameWorld.AddChild(player); + + // 恢复其他游戏状态 + Context.GetModel().Health.Value = saveData.PlayerHealth; + Context.GetModel().CurrentLevel.Value = saveData.CurrentLevel; + Context.GetModel().LoadFromData(saveData.Inventory); + } + + private SaveData CreateSaveData() + { + var player = GetTree().CurrentScene.FindChild("Player"); + + return new SaveData + { + PlayerPosition = player?.Position ?? Vector2.Zero, + PlayerHealth = Context.GetModel().Health.Value, + CurrentLevel = Context.GetModel().CurrentLevel.Value, + Inventory = Context.GetModel().GetData(), + Timestamp = DateTime.Now, + Version = 1 + }; + } +} +``` + +### 自动保存系统 + +```csharp +public class AutoSaveSystem : AbstractSystem +{ + private Timer _autoSaveTimer; + private int _currentSaveSlot; + private float _autoSaveInterval; + + public AutoSaveSystem(float intervalMinutes = 5.0f) + { + _autoSaveInterval = intervalMinutes * 60.0f; + } + + protected override void OnInit() + { + _autoSaveTimer = new Timer(); + _autoSaveTimer.WaitTime = _autoSaveInterval; + _autoSaveTimer.Timeout += OnAutoSave; + _autoSaveTimer.Autostart = true; + + AddChild(_autoSaveTimer); + + // 监听重要事件,立即自动保存 + this.RegisterEvent(OnImportantEvent); + this.RegisterEvent(OnImportantEvent); + this.RegisterEvent(OnImportantEvent); + + Logger.Info("Auto-save system initialized"); + } + + protected override void OnDestroy() + { + _autoSaveTimer?.Stop(); + + // 最后一次自动保存 + PerformAutoSave(); + } + + private void OnAutoSave() + { + PerformAutoSave(); + Logger.Debug("Periodic auto-save completed"); + } + + private void OnImportantEvent(IEvent e) + { + Logger.Info($"Important event {e.GetType().Name}, triggering auto-save"); + PerformAutoSave(); + } + + private void PerformAutoSave() + { + try + { + var saveData = CreateAutoSaveData(); + + // 保存到自动存档槽 + var storage = Context.GetUtility(); + storage.Write("autosave", saveData); + storage.Write("autosave/timestamp", DateTime.Now); + + Logger.Debug("Auto-save completed successfully"); + } + catch (Exception ex) + { + Logger.Error($"Auto-save failed: {ex.Message}"); + } + } + + private SaveData CreateAutoSaveData() + { + return new SaveData + { + // ... 创建保存数据 + }; + } +} +``` + +## 最佳实践 + +### 🏗️ 数据模型设计 + +#### 1. 版本化数据结构 + +```csharp +// 好的做法:版本化数据模型 +[Serializable] +public class PlayerDataV2 +{ + public int Version { get; set; } = 2; + public string Name { get; set; } + public int Health { get; set; } + public int MaxHealth { get; set; } = 100; // V2 新增 + public List Skills { get; set; } = new(); // V2 新增 +} + +// 避免:没有版本控制 +[Serializable] +public class PlayerData +{ + public string Name { get; set; } + public int Health { get; set; } + // 未来新增字段会导致兼容性问题 +} +``` + +#### 2. 数据验证 + +```csharp +public class PlayerData +{ + private string _name; + + public string Name + { + get => _name; + set + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("Player name cannot be empty"); + _name = value; + } + } + + public int Health + { + get => _health; + set => _health = Math.Max(0, value); // 确保不为负数 + } + + public void Validate() + { + if (string.IsNullOrWhiteSpace(Name)) + throw new ValidationException("Player name is required"); + + if (Health < 0) + throw new ValidationException("Health cannot be negative"); + } +} +``` + +### 💾 存储策略 + +#### 1. 分层存储命名 + +```csharp +// 好的做法:有意义的分层结构 +var playerStorage = new ScopedStorage(rootStorage, "players"); +var saveStorage = new ScopedStorage(rootStorage, "saves"); +var settingsStorage = new ScopedStorage(rootStorage, "settings"); +var tempStorage = new ScopedStorage(rootStorage, "temp"); + +// 避免的混乱命名 +var storage1 = new ScopedStorage(rootStorage, "data1"); +var storage2 = new ScopedStorage(rootStorage, "data2"); +``` + +#### 2. 存储性能优化 + +```csharp +// 好的做法:批量操作和缓存 +public class OptimizedDataManager +{ + private readonly IStorage _storage; + private readonly Dictionary _writeBuffer = new(); + + public void QueueWrite(string key, T data) + { + _writeBuffer[key] = data; + } + + public async Task FlushWritesAsync() + { + var tasks = _writeBuffer.Select(kvp => _storage.WriteAsync(kvp.Key, kvp.Value)); + await Task.WhenAll(tasks); + _writeBuffer.Clear(); + } +} + +// 避免:频繁的小写入 +public class InefficientDataManager +{ + public void UpdatePlayerStat(string stat, int value) + { + _storage.Write($"player/stats/{stat}", value); // 每次都写入磁盘 + } +} +``` + +### 🔄 序列化优化 + +#### 1. 选择合适的序列化格式 + +```csharp +// 好的做法:根据需求选择格式 +public class GameSerializer +{ + // JSON:可读性好,调试方便 + public string SerializeForDebug(object data) => JsonConvert.SerializeObject(data, Formatting.Indented); + + // 二进制:体积小,性能好 + public byte[] SerializeForStorage(object data) => MessagePackSerializer.Serialize(data); + + // 压缩:减少存储空间 + public byte[] SerializeWithCompression(object data) + { + var json = JsonConvert.SerializeObject(data); + return Compress(Encoding.UTF8.GetBytes(json)); + } +} +``` + +#### 2. 自定义序列化逻辑 + +```csharp +public class PlayerInventory +{ + public Dictionary Items { get; set; } = new(); + + [JsonIgnore] // 排除不需要序列化的属性 + public int TotalWeight => Items.Sum(kvp => GetItemWeight(kvp.Key) * kvp.Value); + + [JsonProperty("total_items")] // 自定义序列化名称 + public int TotalItems => Items.Values.Sum(); + + public bool ShouldSerializeItems() // 条件序列化 + { + return Items.Count > 0; + } + + [OnDeserialized] // 反序列化后处理 + private void OnDeserialized(StreamingContext context) + { + // 初始化默认值或执行验证 + Items ??= new Dictionary(); + } +} +``` + +### 🏭 模块设计模式 + +#### 1. 单一职责模块 + +```csharp +// 好的做法:模块职责单一 +public class AudioModule : AbstractModule +{ + // 只负责音频相关功能 +} + +public class SaveModule : AbstractModule +{ + // 只负责存档相关功能 +} + +// 避免:功能过于庞大 +public class GameModule : AbstractModule +{ + // 音频、存档、UI、输入全部混在一起 +} +``` + +#### 2. 模块间通信 + +```csharp +public class SaveModule : AbstractModule +{ + public override void Install(IArchitecture architecture) + { + architecture.RegisterUtility(new SaveUtility()); + } +} + +public class PlayerModule : AbstractModule +{ + public override void Install(IArchitecture architecture) + { + architecture.RegisterSystem(new PlayerSystem()); + } + + public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) + { + if (phase == ArchitecturePhase.Ready) + { + // 通过事件进行模块间通信 + this.RegisterEvent(OnPlayerDeath); + } + } + + private void OnPlayerDeath(PlayerDeathEvent e) + { + // 触发保存模块的事件 + Context.SendEvent(new RequestAutoSaveEvent { Reason = "Player Death" }); + } +} +``` + +## 性能特性 + +### 📊 内存管理 + +- **缓存策略**:多层缓存减少磁盘 I/O +- **延迟加载**:按需加载资源,减少内存占用 +- **对象池化**:重用对象,减少 GC 压力 +- **内存映射**:大文件使用内存映射技术 + +### ⚡ 存储性能 + +```csharp +// 性能对比示例 +public class StoragePerformanceTest +{ + // 同步操作:简单直接 + public void SyncWrite(IStorage storage, string key, object data) + { + storage.Write(key, data); // ~1-5ms + } + + // 异步操作:非阻塞 + public async Task AsyncWrite(IStorage storage, string key, object data) + { + await storage.WriteAsync(key, data); // ~0.1-1ms (不阻塞主线程) + } + + // 批量操作:高吞吐量 + public async Task BatchWrite(IStorage storage, Dictionary data) + { + var tasks = data.Select(kvp => storage.WriteAsync(kvp.Key, kvp.Value)); + await Task.WhenAll(tasks); // ~10-50ms for 100 items + } +} +``` + +### 🔄 序列化优化 + +- **格式选择**:JSON(可读性)vs 二进制(性能) +- **压缩技术**:减少存储空间和网络传输 +- **增量序列化**:只序列化变化的部分 +- **版本控制**:向后兼容的数据迁移 + +--- + +## 依赖关系 + +```mermaid +graph TD + A[GFramework.Game] --> B[GFramework.Core] + A --> C[GFramework.Core.Abstractions] + A --> D[Newtonsoft.Json] + A --> E[System.Text.Json] +``` + +## 版本兼容性 + +- **.NET**: 6.0+ +- **Newtonsoft.Json**: 13.0.3+ +- **GFramework.Core**: 与 Core 模块版本保持同步 + +## 许可证 + +本项目基于 Apache 2.0 许可证 - 详情请参阅 [LICENSE](../LICENSE) 文件。 + +--- + +**版本**: 1.0.0 +**更新日期**: 2026-01-12 diff --git a/docs/zh-CN/game/scene-management.md b/docs/zh-CN/game/scene-management.md new file mode 100644 index 00000000..be51d23e --- /dev/null +++ b/docs/zh-CN/game/scene-management.md @@ -0,0 +1,503 @@ +# 场景管理 + +> GFramework.Game 模块提供的场景管理功能,实现游戏场景的加载、切换和状态管理 + +## 概述 + +GFramework.Game 提供了统一的场景管理系统,支持场景的异步加载、切换动画、状态保持等功能。通过场景管理,您可以优雅地处理游戏中的各种场景转换,如主菜单、游戏界面、设置菜单等。 + +## 核心特性 + +- **异步加载**:支持后台异步加载场景,避免卡顿 +- **场景切换动画**:内置淡入淡出等切换效果 +- **状态保持**:场景切换时保持必要的游戏状态 +- **场景栈**:支持场景叠加(如弹出菜单) +- **资源管理**:自动管理场景资源的加载和卸载 + +## 基本用法 + +### 定义场景 + +```csharp +using GFramework.Game.scene; + +public class GameScene : GameSceneBase +{ + protected override void OnLoad() + { + // 场景加载时的初始化 + } + + protected override void OnUnload() + { + // 场景卸载时的清理 + } + + public override void OnEnter() + { + // 进入场景 + } + + public override void OnExit() + { + // 退出场景 + } +} +``` + +### 场景管理器 + +```csharp +using GFramework.Game.scene; + +[ContextAware] +public partial class SceneManager : Node +{ + private ISceneManager _sceneManager; + + public override void _Ready() + { + _sceneManager = Context.GetSystem(); + + // 初始化场景管理器 + _sceneManager.Initialize(); + } + + public async Task SwitchToGameScene() + { + await _sceneManager.SwitchSceneAsync(); + } +} +``` + +## 场景加载 + +### 同步加载 + +适用于小场景的快速切换: + +```csharp +public void LoadMainMenu() +{ + _sceneManager.SwitchScene(); +} +``` + +### 异步加载 + +大场景建议使用异步加载: + +```csharp +public async Task LoadGameLevel(string levelName) +{ + // 显示加载界面 + loadingUI.Show(); + + try + { + // 异步加载场景 + await _sceneManager.SwitchSceneAsync(scene => + { + // 可以在这里传递场景参数 + var gameScene = scene as GameLevelScene; + gameScene.LevelName = levelName; + }); + + GD.Print("场景加载完成"); + } + catch (Exception ex) + { + GD.PrintErr($"场景加载失败: {ex.Message}"); + loadingUI.ShowError(); + } + finally + { + loadingUI.Hide(); + } +} +``` + +### 加载进度 + +监听加载进度: + +```csharp +public async Task LoadSceneWithProgress() where TScene : IScene +{ + loadingUI.Show(); + + var progress = new Progress(value => + { + loadingUI.UpdateProgress(value * 100); + }); + + await _sceneManager.SwitchSceneAsync(progress: progress); + + loadingUI.Hide(); +} +``` + +## 场景切换动画 + +### 内置切换效果 + +```csharp +public async Task SwitchWithFade() +{ + // 使用淡入淡出效果 + await _sceneManager.SwitchSceneAsync( + transition: new FadeTransition( + duration: 0.5f, + fadeColor: Colors.Black + ) + ); +} + +public async Task SlideTransition() +{ + // 使用滑动效果 + await _sceneManager.SwitchSceneAsync( + transition: new SlideTransition( + direction: Vector2.Left, + duration: 0.3f + ) + ); +} +``` + +### 自定义切换动画 + +```csharp +public class CustomTransition : ISceneTransition +{ + public float Duration { get; } = 1.0f; + + public async Task PlayAsync(Node fromScene, Node toScene) + { + // 自定义动画逻辑 + var tween = CreateTween(); + + // 旧场景缩小消失 + tween.TweenProperty(fromScene, "scale", Vector2.Zero, Duration / 2) + .SetTrans(Tween.TransitionType.BackIn); + + await ToSignal(tween, Tween.SignalName.Finished); + + fromScene.Visible = false; + toScene.Visible = true; + toScene.Scale = Vector2.Zero; + + // 新场景放大出现 + tween = CreateTween(); + tween.TweenProperty(toScene, "scale", Vector2.One, Duration / 2) + .SetTrans(Tween.TransitionType.BackOut); + + await ToSignal(tween, Tween.SignalName.Finished); + } +} +``` + +## 场景栈 + +### 推送场景 + +将新场景推入栈中: + +```csharp +public void PushPauseMenu() +{ + _sceneManager.PushScene(); +} +``` + +### 弹出场景 + +弹出栈顶场景: + +```csharp +public void PopScene() +{ + _sceneManager.PopScene(); +} +``` + +### 示例:带返回的导航 + +```csharp +public async Task NavigateToSettings() +{ + // 保存当前场景引用 + var previousScene = _sceneManager.CurrentScene; + + // 推入设置场景 + await _sceneManager.PushSceneAsync(); + + // 设置场景有返回按钮 + await _sceneManager.PushSceneAsync( + data: new ConfirmData + { + Message = "是否保存设置?", + OnConfirm = () => _sceneManager.PopScene(), // 不保存,直接返回 + OnCancel = () => + { + // 保存设置后返回 + SaveSettings(); + _sceneManager.PopScene(); + } + } + ); +} +``` + +## 场景状态管理 + +### 保存状态 + +```csharp +public class GameScene : GameSceneBase +{ + private GameState _state; + + protected override void OnLoad() + { + // 从存档加载状态 + _state = LoadGameState(); + } + + protected override void OnUnload() + { + // 保存状态 + SaveGameState(_state); + } + + public override void OnExit() + { + // 清理工作 + Cleanup(); + } +} +``` + +### 跨场景数据传递 + +```csharp +// 传递数据到新场景 +await _sceneManager.SwitchSceneAsync(data: new GameData +{ + Level = 5, + Difficulty = Difficulty.Hard, + ContinueToken = saveToken +}); + +// 在目标场景中接收数据 +public class GameScene : GameSceneBase +{ + public override void OnEnter(object data) + { + var gameData = data as GameData; + InitializeLevel(gameData.Level, gameData.Difficulty); + } +} +``` + +## 预加载策略 + +### 预加载资源 + +```csharp +public void PreloadCommonResources() +{ + _sceneManager.PreloadScene(); + _sceneManager.PreloadScene(count: 3); +} +``` + +### 预加载配置 + +```csharp +var config = new ScenePreloadConfig +{ + PreloadCount = 5, + Priority = ResourceLoader.CacheMode.Cache, + Timeout = 30.0f +}; + +_sceneManager.ConfigurePreload(config); +``` + +## 资源管理 + +### 自动卸载 + +```csharp +// 配置自动卸载策略 +var unloadConfig = new SceneUnloadConfig +{ + UnloadDelay = 30.0f, // 30秒后卸载 + KeepReferences = true, // 保持引用 + ForceUnloadOnMemoryPressure = true // 内存紧张时强制卸载 +}; + +_sceneManager.ConfigureUnload(unloadConfig); +``` + +### 手动卸载 + +```csharp +// 卸载指定场景 +_sceneManager.UnloadScene(); + +// 卸载所有场景 +_sceneManager.UnloadAllScenes(); +``` + +## 最佳实践 + +### 1. 场景设计原则 + +```csharp +// 推荐:每个场景职责单一 +public class MainMenuScene : GameSceneBase { /* 主菜单 */ } +public class SettingsScene : GameSceneBase { /* 设置菜单 */ } +public class GameHUDScene : GameSceneBase { /* 游戏HUD */ } + +// 避免:场景职责过多 +public class MegaScene : GameSceneBase +{ + /* 包含菜单、设置、HUD、存档管理... 不要这样设计 */ +} +``` + +### 2. 场景切换优化 + +```csharp +// 推荐:使用异步加载 +public async Task LoadGame() +{ + loadingUI.Show("正在加载游戏..."); + await _sceneManager.SwitchSceneAsync(); +} + +// 避免:同步加载大场景 +public void LoadGame() +{ + // 这会导致游戏卡顿 + _sceneManager.SwitchScene(); +} +``` + +### 3. 内存管理 + +```csharp +public class GameScene : GameSceneBase +{ + private Texture2D[] _largeTextures; + + protected override void OnLoad() + { + // 加载资源 + _largeTextures = LoadLargeTextures(); + } + + protected override void OnUnload() + { + // 及时释放大资源 + foreach (var texture in _largeTextures) + { + texture.Dispose(); + } + _largeTextures = null; + } +} +``` + +### 4. 错误处理 + +```csharp +public async Task SafeLoadScene() where TScene : IScene +{ + try + { + await _sceneManager.SwitchSceneAsync(); + } + catch (SceneNotFoundException) + { + Logger.Error($"场景 {typeof(TScene)} 未找到"); + // 回退到默认场景 + await _sceneManager.SwitchSceneAsync(); + } + catch (ResourceLoadException ex) + { + Logger.Error($"资源加载失败: {ex.Message}"); + await ShowRetryDialog(); + } +} +``` + +## 与其他模块集成 + +### 与 Game 模块集成 + +```csharp +public class GameLevelScene : GameSceneBase +{ + private GameManager _gameManager; + + public override void OnEnter() + { + // 初始化游戏管理器 + _gameManager = Context.GetSystem(); + _gameManager.StartGame(); + } + + public override void OnExit() + { + _gameManager.EndGame(); + } +} +``` + +### 与存档系统集成 + +```csharp +public class GameScene : GameSceneBase +{ + private ISaveSystem _saveSystem; + + protected override void OnLoad() + { + _saveSystem = Context.GetUtility(); + + // 检查是否有自动存档 + if (_saveSystem.HasAutoSave()) + { + var dialog = Context.GetSystem(); + await dialog.ShowAsync("发现自动存档,是否继续?", + onConfirm: () => LoadAutoSave(), + onCancel: () => StartNewGame() + ); + } + } + + private void LoadAutoSave() + { + var saveData = _saveSystem.LoadAutoSave(); + RestoreGameState(saveData); + } + + public override void OnExit() + { + // 自动保存 + _saveSystem.SaveAutoSave(CreateSaveData()); + } +} +``` + +--- + +**相关文档**: + +- [Game 概述](./overview) +- [游戏设置](./setting) +- [存储系统](./storage) +- [存档系统](../game/storage) diff --git a/GFramework.Game/setting/README.md b/docs/zh-CN/game/setting.md similarity index 100% rename from GFramework.Game/setting/README.md rename to docs/zh-CN/game/setting.md diff --git a/docs/zh-CN/getting-started/architecture-overview.md b/docs/zh-CN/getting-started/architecture-overview.md new file mode 100644 index 00000000..4e03c825 --- /dev/null +++ b/docs/zh-CN/getting-started/architecture-overview.md @@ -0,0 +1,354 @@ +# 架构概览 + +GFramework 采用经典的五层架构模式,结合 CQRS 和事件驱动设计,为游戏开发提供清晰、可维护的架构基础。 + +## 核心架构模式 + +### 五层架构 + +``` +┌─────────────────────────────────────────┐ +│ View / UI │ ← 用户界面层 +├─────────────────────────────────────────┤ +│ Controller │ ← 控制层 +├─────────────────────────────────────────┤ +│ System │ ← 业务逻辑层 +├─────────────────────────────────────────┤ +│ Model │ ← 数据层 +├─────────────────────────────────────────┤ +│ Utility │ ← 工具层 +└─────────────────────────────────────────┘ +``` + +### 跨层操作机制 + +``` +Command ──┐ +Query ──┼──→ 跨层操作(修改/查询数据) +Event ──┘ +``` + +### 生命周期阶段 + +``` +初始化:Init → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit → BeforeSystemInit → AfterSystemInit → Ready +销毁:Destroy → Destroying → Destroyed +``` + +## 核心组件详解 + +### 1. Architecture(架构) + +应用的中央调度器,负责管理所有组件的生命周期。 + +```csharp +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册所有组件 + RegisterModel(new PlayerModel()); + RegisterSystem(new CombatSystem()); + RegisterUtility(new StorageUtility()); + } +} +``` + +**主要职责:** + +- 组件注册和管理 +- 生命周期协调 +- 依赖注入 +- 跨组件通信协调 + +### 2. Model(数据模型) + +应用的状态存储层,只负责数据的存储和管理。 + +```csharp +public class PlayerModel : AbstractModel +{ + public BindableProperty Health { get; } = new(100); + public BindableProperty Name { get; } = new("Player"); + + protected override void OnInit() + { + // 监听自身数据变化 + Health.Register(OnHealthChanged); + } + + private void OnHealthChanged(int newHealth) + { + if (newHealth <= 0) + this.SendEvent(new PlayerDiedEvent()); + } +} +``` + +**设计原则:** + +- 只存储数据,不包含业务逻辑 +- 使用 BindableProperty 实现响应式数据 +- 通过事件通知数据变化 + +### 3. System(业务系统) + +应用的业务逻辑处理层。 + +```csharp +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() + { + // 监听战斗相关事件 + this.RegisterEvent(OnEnemyAttack); + this.RegisterEvent(OnPlayerAttack); + } + + private void OnEnemyAttack(EnemyAttackEvent e) + { + var playerModel = this.GetModel(); + playerModel.Health.Value -= e.Damage; + this.SendEvent(new PlayerTookDamageEvent { Damage = e.Damage }); + } +} +``` + +**职责范围:** + +- 处理具体的业务逻辑 +- 响应事件并修改模型数据 +- 发送新的事件通知其他组件 + +### 4. Controller(控制器) + +连接 UI 和业务逻辑的桥梁。 + +```csharp +public class PlayerController : IController +{ + private IArchitecture _architecture; + private PlayerModel _playerModel; + + public PlayerController(IArchitecture architecture) + { + _architecture = architecture; + _playerModel = architecture.GetModel(); + + // 监听模型变化并更新 UI + _playerModel.Health.RegisterWithInitValue(UpdateHealthDisplay); + } + + public void OnPlayerInput(Vector2 direction) + { + // 将用户输入转换为命令 + _architecture.SendCommand(new MovePlayerCommand { Direction = direction }); + } + + private void UpdateHealthDisplay(int health) + { + // 更新 UI 显示 + Console.WriteLine($"Player Health: {health}"); + } +} +``` + +**核心功能:** + +- 接收用户输入 +- 发送命令到系统 +- 监听模型变化更新 UI +- 协调 UI 和业务逻辑 + +### 5. Utility(工具类) + +提供无状态的辅助功能。 + +```csharp +public class StorageUtility : IUtility +{ + public void SaveData(string key, T data) + { + // 实现数据保存逻辑 + } + + public T LoadData(string key, T defaultValue = default) + { + // 实现数据加载逻辑 + return defaultValue; + } +} +``` + +**使用场景:** + +- 数据存储和读取 +- 数学计算工具 +- 字符串处理 +- 网络通信辅助 + +## 通信机制 + +### 1. Command(命令) + +用于修改应用状态的操作: + +```csharp +public class MovePlayerCommand : AbstractCommand +{ + public Vector2 Direction { get; set; } + + protected override void OnDo() + { + // 执行移动逻辑 + this.SendEvent(new PlayerMovedEvent { Position = CalculateNewPosition() }); + } +} +``` + +### 2. Query(查询) + +用于查询应用状态: + +```csharp +public class GetPlayerHealthQuery : AbstractQuery +{ + protected override int OnDo() + { + var playerModel = this.GetModel(); + return playerModel.Health.Value; + } +} +``` + +### 3. Event(事件) + +组件间通信的主要机制: + +```csharp +// 发送事件 +this.SendEvent(new PlayerDiedEvent()); + +// 监听事件 +this.RegisterEvent(OnPlayerDied); +``` + +## 响应式编程 + +### BindableProperty + +```csharp +public class PlayerModel : AbstractModel +{ + public BindableProperty Health { get; } = new(100); + public BindableProperty Name { get; } = new("Player"); +} + +// 使用方式 +playerModel.Health.Value = 50; // 自动触发所有监听器 +playerModel.Health.Register(newValue => { + Console.WriteLine($"Health changed to: {newValue}"); +}); +``` + +### 数据绑定优势 + +- **自动更新**:数据变化自动通知监听者 +- **内存安全**:自动管理监听器生命周期 +- **类型安全**:编译时类型检查 +- **性能优化**:只在值真正改变时触发 + +## 最佳实践 + +### 1. 分层职责明确 + +```csharp +// ✅ 正确:Model 只存储数据 +public class PlayerModel : AbstractModel +{ + public BindableProperty Health { get; } = new(100); +} + +// ❌ 错误:Model 包含业务逻辑 +public class PlayerModel : AbstractModel +{ + public void TakeDamage(int damage) // 业务逻辑应该在 System 中 + { + Health.Value -= damage; + } +} +``` + +### 2. 事件驱动设计 + +```csharp +// ✅ 正确:使用事件解耦 +public class CombatSystem : AbstractSystem +{ + private void OnPlayerAttack(PlayerAttackEvent e) + { + // 处理攻击逻辑 + this.SendEvent(new EnemyDamagedEvent { Damage = CalculateDamage() }); + } +} + +// ❌ 错误:直接调用其他组件 +public class CombatSystem : AbstractSystem +{ + private void OnPlayerAttack(PlayerAttackEvent e) + { + var enemySystem = this.GetSystem(); // 紧耦合 + enemySystem.TakeDamage(CalculateDamage()); + } +} +``` + +### 3. 命令查询分离 + +```csharp +// ✅ 正确:明确区分命令和查询 +public class MovePlayerCommand : AbstractCommand { } // 修改状态 +public class GetPlayerPositionQuery : AbstractQuery { } // 查询状态 + +// ❌ 错误:混合读写操作 +public class PlayerManager +{ + public void MoveAndGetPosition(Vector2 direction, out Vector2 position) // 职责不清 + { + // ... + } +} +``` + +## 架构优势 + +### 1. 可维护性 + +- 清晰的职责分离 +- 松耦合的组件设计 +- 易于定位和修复问题 + +### 2. 可测试性 + +- 组件可独立测试 +- 依赖可轻松模拟 +- 支持单元测试和集成测试 + +### 3. 可扩展性 + +- 新功能通过添加组件实现 +- 现有组件无需修改 +- 支持插件化架构 + +### 4. 团队协作 + +- 统一的架构规范 +- 易于新人上手 +- 减少代码冲突 + +## 下一步学习 + +- [深入了解 Architecture 组件](./core/architecture) +- [掌握事件系统](./core/events) +- [学习命令查询模式](./core/command) +- [探索属性系统](./core/property) \ No newline at end of file diff --git a/docs/zh-CN/getting-started/installation.md b/docs/zh-CN/getting-started/installation.md new file mode 100644 index 00000000..5a61a085 --- /dev/null +++ b/docs/zh-CN/getting-started/installation.md @@ -0,0 +1,187 @@ +# 安装配置 + +GFramework 提供多种安装方式,您可以根据项目需求选择合适的包进行安装。 + +## 包选择说明 + +GFramework 采用模块化设计,不同包提供不同的功能: + +| 包名 | 说明 | 适用场景 | +|---------------------------------------|---------|-----------| +| `GeWuYou.GFramework` | 聚合元包 | 快速试用、原型开发 | +| `GeWuYou.GFramework.Core` | 核心框架 | 生产项目推荐 | +| `GeWuYou.GFramework.Game` | 游戏模块 | 需要游戏特定功能 | +| `GeWuYou.GFramework.Godot` | Godot集成 | Godot项目必需 | +| `GeWuYou.GFramework.SourceGenerators` | 源码生成器 | 推荐安装 | + +## 安装方式 + +### 1. 使用 .NET CLI(推荐) + +```bash +# 核心能力(推荐最小起步) +dotnet add package GeWuYou.GFramework.Core +dotnet add package GeWuYou.GFramework.Core.Abstractions + +# 游戏扩展 +dotnet add package GeWuYou.GFramework.Game +dotnet add package GeWuYou.GFramework.Game.Abstractions + +# Godot 集成(仅 Godot 项目需要) +dotnet add package GeWuYou.GFramework.Godot + +# 源码生成器(可选,但推荐) +dotnet add package GeWuYou.GFramework.SourceGenerators +``` + +### 2. 使用 PackageReference + +在您的 `.csproj` 文件中添加: + +```xml + + + net6.0 + + + + + + + + + + + + + + + + + + +``` + +### 3. 使用 NuGet Package Manager + +在 Visual Studio 中: + +1. 右键点击项目 → 管理 NuGet 程序包 +2. 搜索 `GeWuYou.GFramework` +3. 选择需要的包进行安装 + +## 环境要求 + +### 运行时要求 + +- **.NET 6.0** 或更高版本 +- **Godot 4.5+**(仅 Godot 项目) + +### 开发工具 + +- Visual Studio 2022 或 VS Code +- .NET 6.0 SDK +- Godot 4.5+(可选,仅 Godot 项目需要) + +## 项目配置 + +### 1. 基础配置 + +创建 `GlobalUsings.cs` 文件: + +```csharp +global using GFramework.Core; +global using GFramework.Core.architecture; +global using GFramework.Core.command; +global using GFramework.Core.events; +global using GFramework.Core.model; +global using GFramework.Core.property; +global using GFramework.Core.system; +global using GFramework.Core.utility; +``` + +### 2. Godot 项目配置 + +如果使用 Godot 集成,需要在项目设置中启用 C# 支持: + +1. 在 Godot 编辑器中打开项目设置 +2. 导航到 `Mono` → `Editor Settings` +3. 确保启用了 C# 支持 + +### 3. 源码生成器配置 + +源码生成器会自动工作,无需额外配置。如果需要自定义生成器行为,可以在项目文件中添加: + +```xml + + Debug + true + +``` + +## 验证安装 + +创建一个简单的测试来验证安装是否成功: + +```csharp +using GFramework.Core.architecture; + +// 定义简单的架构 +public class TestArchitecture : Architecture +{ + protected override void Init() + { + // 注册一个简单的模型 + RegisterModel(new TestModel()); + } +} + +public class TestModel : AbstractModel +{ + public BindableProperty Message { get; } = new("Hello GFramework!"); +} + +// 测试代码 +var architecture = new TestArchitecture(); +architecture.Initialize(); + +var model = architecture.GetModel(); +Console.WriteLine(model.Message.Value); // 输出: Hello GFramework! +``` + +## 常见问题 + +### 1. 包版本冲突 + +如果遇到版本冲突,建议: + +```bash +dotnet restore --force +dotnet clean +dotnet build +``` + +### 2. Godot 集成问题 + +确保: + +- Godot 版本 >= 4.5 +- 已正确安装 Godot C# 模板 +- 项目引用了正确的 Godot 包 + +### 3. 源码生成器不工作 + +检查: + +- 确保安装了 `GeWuYou.GFramework.SourceGenerators` +- 重启 IDE +- 清理并重新构建项目 + +## 下一步 + +安装完成后,建议: + +1. [快速开始](./getting-started/quick-start) - 构建第一个应用 +2. [架构概览](./getting-started/architecture-overview) - 了解核心概念 +3. [Core 模块文档](./core) - 深入学习核心功能 \ No newline at end of file diff --git a/docs/zh-CN/getting-started/quick-start.md b/docs/zh-CN/getting-started/quick-start.md new file mode 100644 index 00000000..47c7348c --- /dev/null +++ b/docs/zh-CN/getting-started/quick-start.md @@ -0,0 +1,327 @@ +# 快速开始 + +本指南将帮助您快速构建第一个基于 GFramework 的应用程序。 + +## 1. 创建项目架构 + +首先定义您的应用架构: + +```csharp +using GFramework.Core.architecture; + +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册模型 - 存储应用状态 + RegisterModel(new PlayerModel()); + RegisterModel(new GameStateModel()); + + // 注册系统 - 处理业务逻辑 + RegisterSystem(new PlayerSystem()); + RegisterSystem(new GameLogicSystem()); + + // 注册工具类 - 提供辅助功能 + RegisterUtility(new StorageUtility()); + } +} +``` + +## 2. 定义数据模型 + +创建您的数据模型: + +```csharp +public class PlayerModel : AbstractModel +{ + // 使用可绑定属性实现响应式数据 + public BindableProperty Name { get; } = new("Player"); + public BindableProperty Health { get; } = new(100); + public BindableProperty Score { get; } = new(0); + + protected override void OnInit() + { + // 监听健康值变化 + Health.Register(OnHealthChanged); + } + + private void OnHealthChanged(int newHealth) + { + if (newHealth <= 0) + { + this.SendEvent(new PlayerDiedEvent()); + } + } +} + +public class GameStateModel : AbstractModel +{ + public BindableProperty IsGameRunning { get; } = new(false); + public BindableProperty CurrentLevel { get; } = new(1); +} +``` + +## 3. 实现业务逻辑 + +创建处理业务逻辑的系统: + +```csharp +public class PlayerSystem : AbstractSystem +{ + protected override void OnInit() + { + // 监听玩家输入事件 + this.RegisterEvent(OnPlayerMove); + this.RegisterEvent(OnPlayerAttack); + } + + private void OnPlayerMove(PlayerMoveEvent e) + { + var playerModel = this.GetModel(); + // 处理移动逻辑 + Console.WriteLine($"Player moved to {e.Direction}"); + } + + private void OnPlayerAttack(PlayerAttackEvent e) + { + var playerModel = this.GetModel(); + // 处理攻击逻辑 + playerModel.Score.Value += 10; + this.SendEvent(new EnemyDamagedEvent { Damage = 25 }); + } +} + +public class GameLogicSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnEnemyDamaged); + this.RegisterEvent(OnPlayerDied); + } + + private void OnEnemyDamaged(EnemyDamagedEvent e) + { + Console.WriteLine($"Enemy took {e.Damage} damage"); + // 检查是否需要升级关卡 + CheckLevelProgress(); + } + + private void OnPlayerDied(PlayerDiedEvent e) + { + var gameState = this.GetModel(); + gameState.IsGameRunning.Value = false; + Console.WriteLine("Game Over!"); + } + + private void CheckLevelProgress() + { + // 实现关卡进度检查逻辑 + } +} +``` + +## 4. 定义事件 + +创建应用中使用的事件: + +```csharp +public class PlayerMoveEvent : IEvent +{ + public Vector2 Direction { get; set; } +} + +public class PlayerAttackEvent : IEvent +{ + public Vector2 TargetPosition { get; set; } +} + +public class PlayerDiedEvent : IEvent +{ + // 玩家死亡事件 +} + +public class EnemyDamagedEvent : IEvent +{ + public int Damage { get; set; } +} +``` + +## 5. 创建控制器 + +实现控制器来连接 UI 和业务逻辑: + +```csharp +public class GameController : IController +{ + private IArchitecture _architecture; + private PlayerModel _playerModel; + private GameStateModel _gameStateModel; + + public GameController(IArchitecture architecture) + { + _architecture = architecture; + _playerModel = architecture.GetModel(); + _gameStateModel = architecture.GetModel(); + + // 初始化事件监听 + InitializeEventListeners(); + } + + private void InitializeEventListeners() + { + // 监听模型变化并更新 UI + _playerModel.Health.RegisterWithInitValue(OnHealthChanged); + _playerModel.Score.RegisterWithInitValue(OnScoreChanged); + _gameStateModel.IsGameRunning.Register(OnGameStateChanged); + } + + public void StartGame() + { + _gameStateModel.IsGameRunning.Value = true; + _architecture.SendEvent(new GameStartEvent()); + Console.WriteLine("Game started!"); + } + + public void MovePlayer(Vector2 direction) + { + _architecture.SendCommand(new MovePlayerCommand { Direction = direction }); + } + + public void PlayerAttack(Vector2 target) + { + _architecture.SendCommand(new AttackCommand { TargetPosition = target }); + } + + // UI 更新回调 + private void OnHealthChanged(int health) + { + UpdateHealthDisplay(health); + } + + private void OnScoreChanged(int score) + { + UpdateScoreDisplay(score); + } + + private void OnGameStateChanged(bool isRunning) + { + UpdateGameStatusDisplay(isRunning); + } + + private void UpdateHealthDisplay(int health) + { + // 更新血条 UI + Console.WriteLine($"Health: {health}"); + } + + private void UpdateScoreDisplay(int score) + { + // 更新分数显示 + Console.WriteLine($"Score: {score}"); + } + + private void UpdateGameStatusDisplay(bool isRunning) + { + // 更新游戏状态显示 + Console.WriteLine($"Game running: {isRunning}"); + } +} +``` + +## 6. 定义命令 + +创建命令来封装用户操作: + +```csharp +public class MovePlayerCommand : AbstractCommand +{ + public Vector2 Direction { get; set; } + + protected override void OnDo() + { + // 发送移动事件 + this.SendEvent(new PlayerMoveEvent { Direction = Direction }); + } +} + +public class AttackCommand : AbstractCommand +{ + public Vector2 TargetPosition { get; set; } + + protected override void OnDo() + { + // 发送攻击事件 + this.SendEvent(new PlayerAttackEvent { TargetPosition = TargetPosition }); + } +} +``` + +## 7. 运行应用 + +现在让我们运行这个简单的应用: + +```csharp +class Program +{ + static void Main(string[] args) + { + // 创建并初始化架构 + var architecture = new GameArchitecture(); + architecture.Initialize(); + + // 创建控制器 + var gameController = new GameController(architecture); + + // 开始游戏 + gameController.StartGame(); + + // 模拟玩家操作 + gameController.MovePlayer(new Vector2(1, 0)); + gameController.PlayerAttack(new Vector2(5, 5)); + + // 模拟玩家受伤 + var playerModel = architecture.GetModel(); + playerModel.Health.Value = 50; + + // 模拟玩家死亡 + playerModel.Health.Value = 0; + + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(); + } +} +``` + +## 8. 运行结果 + +执行程序后,您应该看到类似以下输出: + +``` +Game started! +Game running: True +Player moved to (1, 0) +Player took 25 damage +Score: 10 +Health: 50 +Health: 0 +Player died +Game Over! +Game running: False +Press any key to exit... +``` + +## 下一步 + +这个简单的示例展示了 GFramework 的核心概念: + +1. **架构模式** - 清晰的分层结构 +2. **响应式数据** - BindableProperty 自动更新 +3. **事件驱动** - 松耦合的组件通信 +4. **命令模式** - 封装用户操作 + +接下来您可以: + +- [深入了解架构组件](./core/architecture) +- [学习事件系统](./core/events) +- [探索 Godot 集成](./godot/overview) +- [查看完整教程](./tutorials/basic-tutorial) \ No newline at end of file diff --git a/docs/zh-CN/godot/coroutine.md b/docs/zh-CN/godot/coroutine.md new file mode 100644 index 00000000..2fcc8ad0 --- /dev/null +++ b/docs/zh-CN/godot/coroutine.md @@ -0,0 +1,406 @@ +# Godot 协程系统 + +> GFramework 在 Godot 引擎中的协程支持,实现异步操作的优雅管理 + +## 概述 + +GFramework.Godot 提供了与 Godot 引擎深度集成的协程系统,让异步编程变得简单直观。通过协程,您可以暂停执行、等待条件满足、或延迟执行操作,而不会阻塞主线程。 + +## 核心特性 + +- **无缝集成**:与 Godot 的 `_Process`、`_Ready` 等生命周期方法完美配合 +- **类型安全**:强类型的协程返回结果处理 +- **自动清理**:协程与节点生命周期自动绑定,避免内存泄漏 +- **丰富的等待条件**:支持等待信号、时间延迟、帧结束等多种条件 + +## 基本用法 + +### 创建协程 + +使用 `StartCoroutine` 方法启动协程: + +```csharp +using GFramework.Godot.coroutine; + +[ContextAware] +public partial class MyNode : Node +{ + public override void _Ready() + { + // 启动协程 + this.StartCoroutine(DoSomethingAsync()); + } + + private System.Collections.IEnumerator DoSomethingAsync() + { + GD.Print("开始执行"); + + // 等待 2 秒 + yield return new WaitForSeconds(2.0f); + + GD.Print("2 秒后继续执行"); + + // 等待下一帧 + yield return new WaitForEndOfFrame(); + + GD.Print("下一帧继续"); + } +} +``` + +### 等待信号 + +协程可以等待 Godot 信号: + +```csharp +private System.Collections.IEnumerator WaitForSignalExample() +{ + GD.Print("等待按钮点击"); + + // 等待按钮被点击 + var button = GetNode