From 6c99b811ecfd74f4d73e292a245be382b3217639 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Tue, 9 Dec 2025 09:06:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor(editor):=20=E7=BB=9F=E4=B8=80=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=AE=A1=E7=90=86=E3=80=81=E5=AE=8C=E5=96=84=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=8D=B8=E8=BD=BD=E5=92=8C=E7=83=AD=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=20(#298)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要变更: 1. 统一配置管理 (EditorConfig) - 新增 EditorConfig 集中管理路径、文件名、全局变量等配置 - 添加 SDK 模块配置系统 (ISDKModuleConfig) - 重构 PluginSDKRegistry 使用配置而非硬编码 2. 完善插件卸载机制 - 扩展 PluginRegisteredResources 追踪运行时资源 - 实现完整的 deactivatePluginRuntime 清理流程 - ComponentRegistry 添加 unregister/getRegisteredComponents 方法 3. 热更新同步机制 - 新增 HotReloadCoordinator 协调热更新过程 - 热更新期间暂停 ECS 循环避免竞态条件 - 支持超时保护和失败恢复 --- packages/core/src/ECS/Component.ts | 3 + .../ComponentStorage/ComponentRegistry.ts | 56 +++ .../editor-app/src/services/PluginLoader.ts | 43 +- .../src/services/PluginSDKRegistry.ts | 190 ++++++--- .../editor-core/src/Config/EditorConfig.ts | 397 ++++++++++++++++++ packages/editor-core/src/Config/index.ts | 5 + .../editor-core/src/Plugin/PluginManager.ts | 175 +++++++- .../Services/UserCode/HotReloadCoordinator.ts | 328 +++++++++++++++ .../src/Services/UserCode/IUserCodeService.ts | 31 +- .../src/Services/UserCode/UserCodeService.ts | 202 ++++++--- .../src/Services/UserCode/index.ts | 9 +- packages/editor-core/src/index.ts | 3 + 12 files changed, 1270 insertions(+), 172 deletions(-) create mode 100644 packages/editor-core/src/Config/EditorConfig.ts create mode 100644 packages/editor-core/src/Config/index.ts create mode 100644 packages/editor-core/src/Services/UserCode/HotReloadCoordinator.ts diff --git a/packages/core/src/ECS/Component.ts b/packages/core/src/ECS/Component.ts index 32566fc2..20e45756 100644 --- a/packages/core/src/ECS/Component.ts +++ b/packages/core/src/ECS/Component.ts @@ -36,6 +36,9 @@ export abstract class Component implements IComponent { * 组件ID生成器 * * 用于为每个组件分配唯一的ID。 + * + * Component ID generator. + * Used to assign unique IDs to each component. */ private static idGenerator: number = 0; diff --git a/packages/core/src/ECS/Core/ComponentStorage/ComponentRegistry.ts b/packages/core/src/ECS/Core/ComponentStorage/ComponentRegistry.ts index 261e0150..a84e0e4e 100644 --- a/packages/core/src/ECS/Core/ComponentStorage/ComponentRegistry.ts +++ b/packages/core/src/ECS/Core/ComponentStorage/ComponentRegistry.ts @@ -259,8 +259,64 @@ export class ComponentRegistry { return this.hotReloadEnabled; } + /** + * 注销组件类型 + * Unregister component type + * + * 用于插件卸载时清理组件。 + * 注意:这不会释放 bitIndex,以避免索引冲突。 + * + * Used for cleanup during plugin unload. + * Note: This does not release bitIndex to avoid index conflicts. + * + * @param componentName 组件名称 | Component name + */ + public static unregister(componentName: string): void { + const componentType = this.componentNameToType.get(componentName); + if (!componentType) { + return; + } + + const bitIndex = this.componentTypes.get(componentType); + + // 移除类型映射 + // Remove type mappings + this.componentTypes.delete(componentType); + if (bitIndex !== undefined) { + this.bitIndexToType.delete(bitIndex); + } + this.componentNameToType.delete(componentName); + this.componentNameToId.delete(componentName); + + // 清除相关的掩码缓存 + // Clear related mask cache + this.clearMaskCache(); + + this._logger.debug(`Component unregistered: ${componentName}`); + } + + /** + * 获取所有已注册的组件信息 + * Get all registered component info + * + * @returns 组件信息数组 | Array of component info + */ + public static getRegisteredComponents(): Array<{ name: string; type: Function; bitIndex: number }> { + const result: Array<{ name: string; type: Function; bitIndex: number }> = []; + + for (const [name, type] of this.componentNameToType) { + const bitIndex = this.componentTypes.get(type); + if (bitIndex !== undefined) { + result.push({ name, type, bitIndex }); + } + } + + return result; + } + /** * 重置注册表(用于测试) + * Reset registry (for testing) */ public static reset(): void { this.componentTypes.clear(); diff --git a/packages/editor-app/src/services/PluginLoader.ts b/packages/editor-app/src/services/PluginLoader.ts index 10b0731d..d254769c 100644 --- a/packages/editor-app/src/services/PluginLoader.ts +++ b/packages/editor-app/src/services/PluginLoader.ts @@ -3,7 +3,7 @@ * Project Plugin Loader */ -import { PluginManager, LocaleService, MessageHub } from '@esengine/editor-core'; +import { PluginManager, LocaleService, MessageHub, EditorConfig, getPluginsPath } from '@esengine/editor-core'; import type { IPlugin, ModuleManifest } from '@esengine/editor-core'; import { Core } from '@esengine/ecs-framework'; import { TauriAPI } from '../api/tauri'; @@ -30,13 +30,19 @@ interface LoadedPluginMeta { scriptElement?: HTMLScriptElement; } +/** + * 插件容器全局变量名 + * Plugin container global variable name + */ +const PLUGINS_GLOBAL_NAME = EditorConfig.globals.plugins; + /** * 项目插件加载器 * * 使用全局变量方案加载插件: * 1. 插件构建时将 @esengine/* 标记为 external - * 2. 插件输出为 IIFE 格式,依赖从 window.__ESENGINE__ 获取 - * 3. 插件导出到 window.__ESENGINE_PLUGINS__[pluginName] + * 2. 插件输出为 IIFE 格式,依赖从全局 SDK 对象获取 + * 3. 插件导出到全局插件容器 */ export class PluginLoader { private loadedPlugins: Map = new Map(); @@ -51,7 +57,7 @@ export class PluginLoader { // 初始化插件容器 this.initPluginContainer(); - const pluginsPath = `${projectPath}/plugins`; + const pluginsPath = getPluginsPath(projectPath); try { const exists = await TauriAPI.pathExists(pluginsPath); @@ -75,8 +81,8 @@ export class PluginLoader { * 初始化插件容器 */ private initPluginContainer(): void { - if (!window.__ESENGINE_PLUGINS__) { - window.__ESENGINE_PLUGINS__ = {}; + if (!(window as any)[PLUGINS_GLOBAL_NAME]) { + (window as any)[PLUGINS_GLOBAL_NAME] = {}; } } @@ -161,25 +167,27 @@ export class PluginLoader { ): Promise { const pluginKey = this.sanitizePluginKey(pluginName); + const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record; + try { - // 插件代码是 IIFE 格式,会自动导出到 window.__ESENGINE_PLUGINS__ + // 插件代码是 IIFE 格式,会自动导出到全局插件容器 await this.executeViaScriptTag(code, pluginName); // 从全局容器获取插件模块 - const pluginModule = window.__ESENGINE_PLUGINS__[pluginKey]; + const pluginModule = pluginsContainer[pluginKey]; if (!pluginModule) { // 尝试其他可能的 key 格式 - const altKeys = Object.keys(window.__ESENGINE_PLUGINS__).filter(k => + const altKeys = Object.keys(pluginsContainer).filter(k => k.includes(pluginName.replace(/@/g, '').replace(/\//g, '_').replace(/-/g, '_')) ); if (altKeys.length > 0 && altKeys[0] !== undefined) { const foundKey = altKeys[0]; - const altModule = window.__ESENGINE_PLUGINS__[foundKey]; + const altModule = pluginsContainer[foundKey]; return this.findPluginLoader(altModule); } - console.error(`[PluginLoader] Plugin ${pluginName} did not export to __ESENGINE_PLUGINS__`); + console.error(`[PluginLoader] Plugin ${pluginName} did not export to ${PLUGINS_GLOBAL_NAME}`); return null; } @@ -330,11 +338,13 @@ export class PluginLoader { * 卸载所有已加载的插件 */ async unloadProjectPlugins(_pluginManager: PluginManager): Promise { + const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record | undefined; + for (const pluginName of this.loadedPlugins.keys()) { // 清理全局容器中的插件 const pluginKey = this.sanitizePluginKey(pluginName); - if (window.__ESENGINE_PLUGINS__?.[pluginKey]) { - delete window.__ESENGINE_PLUGINS__[pluginKey]; + if (pluginsContainer?.[pluginKey]) { + delete pluginsContainer[pluginKey]; } // 移除 script 标签 @@ -353,10 +363,3 @@ export class PluginLoader { return Array.from(this.loadedPlugins.keys()); } } - -// 全局类型声明 -declare global { - interface Window { - __ESENGINE_PLUGINS__: Record; - } -} diff --git a/packages/editor-app/src/services/PluginSDKRegistry.ts b/packages/editor-app/src/services/PluginSDKRegistry.ts index f92393b3..82f48461 100644 --- a/packages/editor-app/src/services/PluginSDKRegistry.ts +++ b/packages/editor-app/src/services/PluginSDKRegistry.ts @@ -5,96 +5,122 @@ * 将编辑器核心模块暴露为全局变量,供插件使用。 * 插件构建时将这些模块标记为 external,运行时从全局对象获取。 * + * Exposes editor core modules as global variables for plugin use. + * Plugins mark these modules as external during build, and access them from global object at runtime. + * * 使用方式: * 1. 编辑器启动时调用 PluginSDKRegistry.initialize() - * 2. 插件构建配置中设置 external: ['@esengine/editor-runtime', ...] - * 3. 插件构建配置中设置 globals: { '@esengine/editor-runtime': '__ESENGINE__.editorRuntime' } + * 2. 插件构建配置中设置 external: getSDKPackageNames() + * 3. 插件构建配置中设置 globals: getSDKGlobalsMapping() */ import { Core } from '@esengine/ecs-framework'; -import { EntityStoreService, MessageHub } from '@esengine/editor-core'; +import { + EntityStoreService, + MessageHub, + EditorConfig, + getSDKGlobalsMapping, + getSDKPackageNames, + getEnabledSDKModules, + type ISDKModuleConfig +} from '@esengine/editor-core'; -// 导入所有需要暴露给插件的模块 -import * as editorRuntime from '@esengine/editor-runtime'; +// 动态导入所有 SDK 模块 +// Dynamic import all SDK modules import * as ecsFramework from '@esengine/ecs-framework'; +import * as editorRuntime from '@esengine/editor-runtime'; import * as behaviorTree from '@esengine/behavior-tree'; import * as engineCore from '@esengine/engine-core'; import * as sprite from '@esengine/sprite'; import * as camera from '@esengine/camera'; import * as audio from '@esengine/audio'; -// 存储服务实例引用(在初始化时设置) -let entityStoreInstance: EntityStoreService | null = null; -let messageHubInstance: MessageHub | null = null; - -// SDK 模块映射 -const SDK_MODULES = { - '@esengine/editor-runtime': editorRuntime, +/** + * 模块实例映射 + * Module instance mapping + * + * 由于 ES 模块的静态导入限制,我们需要维护一个包名到模块的映射。 + * Due to ES module static import limitations, we need to maintain a mapping from package name to module. + */ +const MODULE_INSTANCES: Record = { '@esengine/ecs-framework': ecsFramework, + '@esengine/editor-runtime': editorRuntime, '@esengine/behavior-tree': behaviorTree, '@esengine/engine-core': engineCore, '@esengine/sprite': sprite, '@esengine/camera': camera, '@esengine/audio': audio, -} as const; +}; -// 全局变量名称映射(用于插件构建配置) -export const SDK_GLOBALS = { - '@esengine/editor-runtime': '__ESENGINE__.editorRuntime', - '@esengine/ecs-framework': '__ESENGINE__.ecsFramework', - '@esengine/behavior-tree': '__ESENGINE__.behaviorTree', - '@esengine/engine-core': '__ESENGINE__.engineCore', - '@esengine/sprite': '__ESENGINE__.sprite', - '@esengine/camera': '__ESENGINE__.camera', - '@esengine/audio': '__ESENGINE__.audio', -} as const; +// 存储服务实例引用(在初始化时设置) +// Service instance references (set during initialization) +let entityStoreInstance: EntityStoreService | null = null; +let messageHubInstance: MessageHub | null = null; /** * 插件 API 接口 - * 为插件提供统一的访问接口,避免模块实例不一致的问题 + * Plugin API interface + * + * 为插件提供统一的访问接口,避免模块实例不一致的问题。 + * Provides unified access interface for plugins, avoiding module instance inconsistency issues. */ export interface IPluginAPI { - /** 获取当前场景 */ + /** 获取当前场景 | Get current scene */ getScene(): any; - /** 获取 EntityStoreService */ + /** 获取 EntityStoreService | Get EntityStoreService */ getEntityStore(): EntityStoreService; - /** 获取 MessageHub */ + /** 获取 MessageHub | Get MessageHub */ getMessageHub(): MessageHub; - /** 解析服务 */ + /** 解析服务 | Resolve service */ resolveService(serviceType: any): T; - /** 获取 Core 实例 */ + /** 获取 Core 实例 | Get Core instance */ getCore(): typeof Core; } -// 扩展 Window.__ESENGINE__ 类型(基础类型已在 PluginAPI.ts 中定义) -interface ESEngineGlobal { - editorRuntime: typeof editorRuntime; - ecsFramework: typeof ecsFramework; - behaviorTree: typeof behaviorTree; - engineCore: typeof engineCore; - sprite: typeof sprite; - camera: typeof camera; - audio: typeof audio; +/** + * SDK 全局对象类型 + * SDK global object type + */ +export interface ISDKGlobal { + /** 动态模块加载 | Dynamic module loading */ require: (moduleName: string) => any; + /** 插件 API | Plugin API */ api: IPluginAPI; + /** 其他动态注册的模块 | Other dynamically registered modules */ + [key: string]: any; } /** * 插件 SDK 注册器 + * Plugin SDK Registry + * + * 职责: + * 1. 将 SDK 模块暴露到全局对象 + * 2. 提供插件 API + * 3. 支持动态模块加载 + * + * Responsibilities: + * 1. Expose SDK modules to global object + * 2. Provide plugin API + * 3. Support dynamic module loading */ export class PluginSDKRegistry { private static initialized = false; /** * 初始化 SDK 注册器 - * 将所有 SDK 模块暴露到全局对象 + * Initialize SDK registry + * + * 将所有配置的 SDK 模块暴露到全局对象。 + * Exposes all configured SDK modules to global object. */ static initialize(): void { if (this.initialized) { return; } - // 获取服务实例(使用编辑器内部的类型,确保类型匹配) + // 获取服务实例 + // Get service instances entityStoreInstance = Core.services.resolve(EntityStoreService); messageHubInstance = Core.services.resolve(MessageHub); @@ -105,8 +131,47 @@ export class PluginSDKRegistry { console.error('[PluginSDKRegistry] MessageHub not registered yet!'); } - // 创建插件 API - 直接返回实例引用,避免类型匹配问题 - const pluginAPI: IPluginAPI = { + // 创建 SDK 全局对象 + // Create SDK global object + const sdkGlobal: ISDKGlobal = { + require: this.requireModule.bind(this), + api: this.createPluginAPI(), + }; + + // 从配置自动注册所有启用的模块 + // Auto-register all enabled modules from config + const enabledModules = getEnabledSDKModules(); + for (const config of enabledModules) { + const moduleInstance = MODULE_INSTANCES[config.packageName]; + if (moduleInstance) { + sdkGlobal[config.globalKey] = moduleInstance; + } else { + console.warn( + `[PluginSDKRegistry] Module "${config.packageName}" configured but not imported. ` + + `Please add import statement for this module.` + ); + } + } + + // 设置全局对象 + // Set global object + const sdkGlobalName = EditorConfig.globals.sdk; + (window as any)[sdkGlobalName] = sdkGlobal; + + this.initialized = true; + + console.log( + `[PluginSDKRegistry] Initialized with ${enabledModules.length} modules:`, + enabledModules.map(m => m.globalKey) + ); + } + + /** + * 创建插件 API + * Create plugin API + */ + private static createPluginAPI(): IPluginAPI { + return { getScene: () => Core.scene, getEntityStore: () => { if (!entityStoreInstance) { @@ -123,36 +188,29 @@ export class PluginSDKRegistry { resolveService: (serviceType: any): T => Core.services.resolve(serviceType) as T, getCore: () => Core, }; - - // 创建全局命名空间 - window.__ESENGINE__ = { - editorRuntime, - ecsFramework, - behaviorTree, - engineCore, - sprite, - camera, - audio, - require: this.requireModule.bind(this), - api: pluginAPI, - }; - - this.initialized = true; } /** * 动态获取模块(用于 CommonJS 风格的插件) + * Dynamic module loading (for CommonJS style plugins) + * + * @param moduleName 模块包名 | Module package name */ private static requireModule(moduleName: string): any { - const module = SDK_MODULES[moduleName as keyof typeof SDK_MODULES]; + const module = MODULE_INSTANCES[moduleName]; if (!module) { - throw new Error(`[PluginSDKRegistry] Unknown module: ${moduleName}`); + const availableModules = Object.keys(MODULE_INSTANCES).join(', '); + throw new Error( + `[PluginSDKRegistry] Unknown module: "${moduleName}". ` + + `Available modules: ${availableModules}` + ); } return module; } /** * 检查是否已初始化 + * Check if initialized */ static isInitialized(): boolean { return this.initialized; @@ -160,15 +218,25 @@ export class PluginSDKRegistry { /** * 获取所有可用的 SDK 模块名称 + * Get all available SDK module names + * + * @deprecated 使用 getSDKPackageNames() 代替 | Use getSDKPackageNames() instead */ static getAvailableModules(): string[] { - return Object.keys(SDK_MODULES); + return getSDKPackageNames(); } /** * 获取全局变量映射(用于生成插件构建配置) + * Get globals config (for generating plugin build config) + * + * @deprecated 使用 getSDKGlobalsMapping() 代替 | Use getSDKGlobalsMapping() instead */ static getGlobalsConfig(): Record { - return { ...SDK_GLOBALS }; + return getSDKGlobalsMapping(); } } + +// 重新导出辅助函数,方便插件构建工具使用 +// Re-export helper functions for plugin build tools +export { getSDKGlobalsMapping, getSDKPackageNames, getEnabledSDKModules }; diff --git a/packages/editor-core/src/Config/EditorConfig.ts b/packages/editor-core/src/Config/EditorConfig.ts new file mode 100644 index 00000000..143dd560 --- /dev/null +++ b/packages/editor-core/src/Config/EditorConfig.ts @@ -0,0 +1,397 @@ +/** + * 编辑器配置 + * + * 集中管理所有编辑器相关的路径、文件名、全局变量等配置。 + * 避免硬编码分散在各处,提高可维护性和可配置性。 + * + * Editor configuration. + * Centralized management of all editor-related paths, filenames, and global variables. + * Avoids scattered hardcoding, improving maintainability and configurability. + */ + +/** + * 路径配置 + * Path configuration + */ +export interface IPathConfig { + /** 用户脚本目录 | User scripts directory */ + readonly scripts: string; + /** 编辑器脚本子目录 | Editor scripts subdirectory */ + readonly editorScripts: string; + /** 编译输出目录 | Compiled output directory */ + readonly compiled: string; + /** 插件目录 | Plugins directory */ + readonly plugins: string; + /** 资源目录 | Assets directory */ + readonly assets: string; + /** 引擎模块目录 | Engine modules directory */ + readonly engineModules: string; +} + +/** + * 输出文件配置 + * Output file configuration + */ +export interface IOutputConfig { + /** 运行时代码包 | Runtime bundle filename */ + readonly runtimeBundle: string; + /** 编辑器代码包 | Editor bundle filename */ + readonly editorBundle: string; +} + +/** + * 全局变量名配置 + * Global variable names configuration + */ +export interface IGlobalsConfig { + /** SDK 全局对象名 | SDK global object name */ + readonly sdk: string; + /** 插件容器全局对象名 | Plugins container global object name */ + readonly plugins: string; + /** 用户运行时导出全局变量名 | User runtime exports global variable name */ + readonly userRuntimeExports: string; + /** 用户编辑器导出全局变量名 | User editor exports global variable name */ + readonly userEditorExports: string; +} + +/** + * 项目配置文件名 + * Project configuration filenames + */ +export interface IProjectFilesConfig { + /** 项目配置文件 | Project configuration file */ + readonly projectConfig: string; + /** 模块索引文件 | Module index file */ + readonly moduleIndex: string; + /** 模块清单文件 | Module manifest file */ + readonly moduleManifest: string; +} + +/** + * 包名配置 + * Package name configuration + */ +export interface IPackageConfig { + /** 包作用域 | Package scope */ + readonly scope: string; + /** 核心框架包名 | Core framework package name */ + readonly coreFramework: string; +} + +/** + * 类型标记配置 + * Type marker configuration + * + * 用于标记用户代码相关的运行时属性。 + * 注意:组件和系统的类型检测使用 @ECSComponent/@ECSSystem 装饰器的 Symbol 键。 + * + * Used for marking user code related runtime properties. + * Note: Component and System type detection uses Symbol keys from @ECSComponent/@ECSSystem decorators. + */ +export interface ITypeMarkersConfig { + /** 用户系统标记 | User system marker */ + readonly userSystem: string; + /** 用户系统名称属性 | User system name property */ + readonly userSystemName: string; +} + +/** + * SDK 模块类型 + * SDK module type + */ +export type SDKModuleType = 'core' | 'runtime' | 'editor'; + +/** + * SDK 模块配置 + * SDK module configuration + * + * 定义暴露给插件和用户代码的 SDK 模块。 + * Defines SDK modules exposed to plugins and user code. + */ +export interface ISDKModuleConfig { + /** + * 包名 + * Package name + * @example '@esengine/ecs-framework' + */ + readonly packageName: string; + + /** + * 全局变量键名 + * Global variable key name + * @example 'ecsFramework' -> window.__ESENGINE__.ecsFramework + */ + readonly globalKey: string; + + /** + * 模块类型 + * Module type + * - core: 核心模块,必须加载 + * - runtime: 运行时模块,游戏运行时可用 + * - editor: 编辑器模块,仅编辑器环境可用 + */ + readonly type: SDKModuleType; + + /** + * 是否启用(默认 true) + * Whether enabled (default true) + */ + readonly enabled?: boolean; +} + +/** + * 编辑器配置接口 + * Editor configuration interface + */ +export interface IEditorConfig { + readonly paths: IPathConfig; + readonly output: IOutputConfig; + readonly globals: IGlobalsConfig; + readonly projectFiles: IProjectFilesConfig; + readonly package: IPackageConfig; + readonly typeMarkers: ITypeMarkersConfig; + readonly sdkModules: readonly ISDKModuleConfig[]; +} + +/** + * 默认编辑器配置 + * Default editor configuration + */ +export const EditorConfig: IEditorConfig = { + paths: { + scripts: 'scripts', + editorScripts: 'editor', + compiled: '.esengine/compiled', + plugins: 'plugins', + assets: 'assets', + engineModules: 'engine', + }, + + output: { + runtimeBundle: 'user-runtime.js', + editorBundle: 'user-editor.js', + }, + + globals: { + sdk: '__ESENGINE__', + plugins: '__ESENGINE_PLUGINS__', + userRuntimeExports: '__USER_RUNTIME_EXPORTS__', + userEditorExports: '__USER_EDITOR_EXPORTS__', + }, + + projectFiles: { + projectConfig: 'esengine.project.json', + moduleIndex: 'index.json', + moduleManifest: 'module.json', + }, + + package: { + scope: '@esengine', + coreFramework: '@esengine/ecs-framework', + }, + + typeMarkers: { + userSystem: '__isUserSystem__', + userSystemName: '__userSystemName__', + }, + + sdkModules: [ + // 核心模块 - 必须加载 + // Core modules - must be loaded + { packageName: '@esengine/ecs-framework', globalKey: 'ecsFramework', type: 'core' }, + + // 运行时模块 - 游戏运行时可用 + // Runtime modules - available at game runtime + { packageName: '@esengine/engine-core', globalKey: 'engineCore', type: 'runtime' }, + { packageName: '@esengine/behavior-tree', globalKey: 'behaviorTree', type: 'runtime' }, + { packageName: '@esengine/sprite', globalKey: 'sprite', type: 'runtime' }, + { packageName: '@esengine/camera', globalKey: 'camera', type: 'runtime' }, + { packageName: '@esengine/audio', globalKey: 'audio', type: 'runtime' }, + + // 编辑器模块 - 仅编辑器环境可用 + // Editor modules - only available in editor environment + { packageName: '@esengine/editor-runtime', globalKey: 'editorRuntime', type: 'editor' }, + ], +} as const; + +/** + * 获取完整的脚本目录路径 + * Get full scripts directory path + * + * @param projectPath 项目根路径 | Project root path + */ +export function getScriptsPath(projectPath: string): string { + return `${projectPath}/${EditorConfig.paths.scripts}`; +} + +/** + * 获取编辑器脚本目录路径 + * Get editor scripts directory path + * + * @param projectPath 项目根路径 | Project root path + */ +export function getEditorScriptsPath(projectPath: string): string { + return `${projectPath}/${EditorConfig.paths.scripts}/${EditorConfig.paths.editorScripts}`; +} + +/** + * 获取编译输出目录路径 + * Get compiled output directory path + * + * @param projectPath 项目根路径 | Project root path + */ +export function getCompiledPath(projectPath: string): string { + return `${projectPath}/${EditorConfig.paths.compiled}`; +} + +/** + * 获取运行时包输出路径 + * Get runtime bundle output path + * + * @param projectPath 项目根路径 | Project root path + */ +export function getRuntimeBundlePath(projectPath: string): string { + return `${getCompiledPath(projectPath)}/${EditorConfig.output.runtimeBundle}`; +} + +/** + * 获取编辑器包输出路径 + * Get editor bundle output path + * + * @param projectPath 项目根路径 | Project root path + */ +export function getEditorBundlePath(projectPath: string): string { + return `${getCompiledPath(projectPath)}/${EditorConfig.output.editorBundle}`; +} + +/** + * 获取插件目录路径 + * Get plugins directory path + * + * @param projectPath 项目根路径 | Project root path + */ +export function getPluginsPath(projectPath: string): string { + return `${projectPath}/${EditorConfig.paths.plugins}`; +} + +/** + * 获取项目配置文件路径 + * Get project configuration file path + * + * @param projectPath 项目根路径 | Project root path + */ +export function getProjectConfigPath(projectPath: string): string { + return `${projectPath}/${EditorConfig.projectFiles.projectConfig}`; +} + +/** + * 获取引擎模块目录路径 + * Get engine modules directory path + * + * @param projectPath 项目根路径 | Project root path + */ +export function getEngineModulesPath(projectPath: string): string { + return `${projectPath}/${EditorConfig.paths.engineModules}`; +} + +/** + * 规范化依赖 ID + * Normalize dependency ID + * + * @param depId 依赖 ID | Dependency ID + * @returns 完整的包名 | Full package name + */ +export function normalizeDependencyId(depId: string): string { + if (depId.startsWith('@')) { + return depId; + } + return `${EditorConfig.package.scope}/${depId}`; +} + +/** + * 获取短依赖 ID(移除作用域) + * Get short dependency ID (remove scope) + * + * @param depId 依赖 ID | Dependency ID + * @returns 短 ID | Short ID + */ +export function getShortDependencyId(depId: string): string { + const prefix = `${EditorConfig.package.scope}/`; + if (depId.startsWith(prefix)) { + return depId.substring(prefix.length); + } + return depId; +} + +/** + * 检查是否为引擎内置包 + * Check if package is engine built-in + * + * @param packageName 包名 | Package name + */ +export function isEnginePackage(packageName: string): boolean { + return packageName.startsWith(EditorConfig.package.scope); +} + +// ==================== SDK 模块辅助函数 ==================== +// SDK module helper functions + +/** + * 获取所有 SDK 模块配置 + * Get all SDK module configurations + */ +export function getSDKModules(): readonly ISDKModuleConfig[] { + return EditorConfig.sdkModules; +} + +/** + * 获取启用的 SDK 模块 + * Get enabled SDK modules + * + * @param type 可选的模块类型过滤 | Optional module type filter + */ +export function getEnabledSDKModules(type?: SDKModuleType): readonly ISDKModuleConfig[] { + return EditorConfig.sdkModules.filter(m => + m.enabled !== false && (type === undefined || m.type === type) + ); +} + +/** + * 获取 SDK 模块的全局变量映射 + * Get SDK modules global variable mapping + * + * 用于生成插件构建配置的 globals 选项。 + * Used for generating plugins build config globals option. + * + * @returns 包名到全局变量路径的映射 | Mapping from package name to global variable path + * @example + * { + * '@esengine/ecs-framework': '__ESENGINE__.ecsFramework', + * '@esengine/behavior-tree': '__ESENGINE__.behaviorTree', + * } + */ +export function getSDKGlobalsMapping(): Record { + const sdkGlobalName = EditorConfig.globals.sdk; + const mapping: Record = {}; + + for (const module of EditorConfig.sdkModules) { + if (module.enabled !== false) { + mapping[module.packageName] = `${sdkGlobalName}.${module.globalKey}`; + } + } + + return mapping; +} + +/** + * 获取所有 SDK 包名列表 + * Get all SDK package names + * + * 用于生成插件构建配置的 external 选项。 + * Used for generating plugins build config external option. + */ +export function getSDKPackageNames(): string[] { + return EditorConfig.sdkModules + .filter(m => m.enabled !== false) + .map(m => m.packageName); +} diff --git a/packages/editor-core/src/Config/index.ts b/packages/editor-core/src/Config/index.ts new file mode 100644 index 00000000..cd0f06c6 --- /dev/null +++ b/packages/editor-core/src/Config/index.ts @@ -0,0 +1,5 @@ +/** + * 配置模块导出 + * Configuration module exports + */ +export * from './EditorConfig'; diff --git a/packages/editor-core/src/Plugin/PluginManager.ts b/packages/editor-core/src/Plugin/PluginManager.ts index 48defbe6..1ceaf520 100644 --- a/packages/editor-core/src/Plugin/PluginManager.ts +++ b/packages/editor-core/src/Plugin/PluginManager.ts @@ -21,6 +21,8 @@ import { FileActionRegistry } from '../Services/FileActionRegistry'; import { UIRegistry } from '../Services/UIRegistry'; import { MessageHub } from '../Services/MessageHub'; import { moduleRegistry } from '../Services/Module/ModuleRegistry'; +import { SerializerRegistry } from '../Services/SerializerRegistry'; +import { ComponentRegistry as EditorComponentRegistry } from '../Services/ComponentRegistry'; const logger = createLogger('PluginManager'); @@ -71,6 +73,9 @@ export interface NormalizedPlugin { * Resources registered by plugin (for cleanup on unload) */ export interface PluginRegisteredResources { + // ==================== 编辑器资源 ==================== + // Editor resources + /** 注册的面板ID | Registered panel IDs */ panelIds: string[]; /** 注册的菜单ID | Registered menu IDs */ @@ -85,6 +90,16 @@ export interface PluginRegisteredResources { fileHandlers: any[]; /** 注册的文件模板 | Registered file templates */ fileTemplates: any[]; + + // ==================== 运行时资源 ==================== + // Runtime resources + + /** 注册的组件类型名称 | Registered component type names */ + componentTypeNames: string[]; + /** 注册的系统实例 | Registered system instances */ + systemInstances: any[]; + /** 注册的序列化器类型 | Registered serializer types */ + serializerTypes: Array<{ pluginName: string; type: string }>; } /** @@ -407,14 +422,20 @@ export class PluginManager implements IService { logger.info(`activatePluginEditor: activating ${pluginId}`); // 初始化资源跟踪 + // Initialize resource tracking const resources: PluginRegisteredResources = { + // 编辑器资源 | Editor resources panelIds: [], menuIds: [], toolbarIds: [], entityTemplateIds: [], componentActions: [], fileHandlers: [], - fileTemplates: [] + fileTemplates: [], + // 运行时资源 | Runtime resources + componentTypeNames: [], + systemInstances: [], + serializerTypes: [], }; // 获取注册表服务 @@ -627,31 +648,73 @@ export class PluginManager implements IService { const runtimeModule = plugin.plugin.runtimeModule; if (!runtimeModule) return; - // 注册组件 - if (runtimeModule.registerComponents) { - runtimeModule.registerComponents(ComponentRegistry); - logger.debug(`Components registered for: ${pluginId}`); + // 确保资源跟踪对象存在 + // Ensure resource tracking object exists + if (!plugin.registeredResources) { + plugin.registeredResources = { + panelIds: [], + menuIds: [], + toolbarIds: [], + entityTemplateIds: [], + componentActions: [], + fileHandlers: [], + fileTemplates: [], + componentTypeNames: [], + systemInstances: [], + serializerTypes: [], + }; } - // 注册服务 + const resources = plugin.registeredResources; + + // 注册组件(使用包装的 Registry 来跟踪) + // Register components (use wrapped registry to track) + if (runtimeModule.registerComponents) { + const componentsBefore = new Set(ComponentRegistry.getRegisteredComponents().map(c => c.name)); + runtimeModule.registerComponents(ComponentRegistry); + const componentsAfter = ComponentRegistry.getRegisteredComponents(); + + // 跟踪新注册的组件 + // Track newly registered components + for (const comp of componentsAfter) { + if (!componentsBefore.has(comp.name)) { + resources.componentTypeNames.push(comp.name); + } + } + logger.debug(`Components registered for: ${pluginId} (${resources.componentTypeNames.length} new)`); + } + + // 注册服务(服务目前无法卸载,记录日志即可) + // Register services (services cannot be unloaded currently, just log) if (runtimeModule.registerServices) { runtimeModule.registerServices(this.services); logger.debug(`Services registered for: ${pluginId}`); } - // 创建系统 + // 创建系统(跟踪创建的系统) + // Create systems (track created systems) if (runtimeModule.createSystems) { + const systemsBefore = this.currentScene.systems.length; runtimeModule.createSystems(this.currentScene, this.currentContext); - logger.debug(`Systems created for: ${pluginId}`); + const systemsAfter = this.currentScene.systems; + + // 跟踪新创建的系统 + // Track newly created systems + for (let i = systemsBefore; i < systemsAfter.length; i++) { + resources.systemInstances.push(systemsAfter[i]); + } + logger.debug(`Systems created for: ${pluginId} (${systemsAfter.length - systemsBefore} new)`); } // 调用系统创建后回调 + // Call post-creation callback if (runtimeModule.onSystemsCreated) { runtimeModule.onSystemsCreated(this.currentScene, this.currentContext); logger.debug(`Systems wired for: ${pluginId}`); } // 调用初始化 + // Call initialization if (runtimeModule.onInitialize) { await runtimeModule.onInitialize(); logger.debug(`Runtime initialized for: ${pluginId}`); @@ -661,22 +724,104 @@ export class PluginManager implements IService { /** * 动态卸载插件的运行时模块 * Dynamically deactivate plugin's runtime module + * + * 卸载顺序(与激活相反): + * 1. 调用 onDestroy 回调 + * 2. 移除系统 + * 3. 注销组件 + * 4. 清理序列化器 + * + * Unload order (reverse of activation): + * 1. Call onDestroy callback + * 2. Remove systems + * 3. Unregister components + * 4. Cleanup serializers */ private deactivatePluginRuntime(pluginId: string): void { const plugin = this.plugins.get(pluginId); if (!plugin) return; const runtimeModule = plugin.plugin.runtimeModule; - if (!runtimeModule) return; + const resources = plugin.registeredResources; - // 调用销毁回调 - if (runtimeModule.onDestroy) { - runtimeModule.onDestroy(); - logger.debug(`Runtime destroyed for: ${pluginId}`); + // 1. 调用销毁回调 + // Step 1: Call destroy callback + if (runtimeModule?.onDestroy) { + try { + runtimeModule.onDestroy(); + logger.debug(`Runtime onDestroy called for: ${pluginId}`); + } catch (e) { + logger.error(`Error in onDestroy for ${pluginId}:`, e); + } } - // 注意:组件和服务无法动态注销,这是设计限制 - // 系统的移除需要场景支持,暂时只调用 onDestroy + if (!resources) { + logger.debug(`No resources to cleanup for: ${pluginId}`); + return; + } + + // 2. 移除系统 + // Step 2: Remove systems + if (this.currentScene && resources.systemInstances.length > 0) { + for (const system of resources.systemInstances) { + try { + this.currentScene.removeSystem(system); + logger.debug(`System removed: ${system.constructor?.name || 'Unknown'}`); + } catch (e) { + logger.error(`Failed to remove system:`, e); + } + } + resources.systemInstances = []; + } + + // 3. 注销组件(从 Core 的 ComponentRegistry) + // Step 3: Unregister components (from Core's ComponentRegistry) + if (resources.componentTypeNames.length > 0) { + for (const componentName of resources.componentTypeNames) { + try { + ComponentRegistry.unregister(componentName); + logger.debug(`Component unregistered: ${componentName}`); + } catch (e) { + logger.error(`Failed to unregister component ${componentName}:`, e); + } + } + + // 同时从编辑器的 ComponentRegistry 注销 + // Also unregister from editor's ComponentRegistry + if (this.services) { + const editorComponentRegistry = this.services.tryResolve(EditorComponentRegistry); + if (editorComponentRegistry) { + for (const componentName of resources.componentTypeNames) { + try { + editorComponentRegistry.unregister(componentName); + } catch (e) { + // 忽略,可能未注册到编辑器注册表 + // Ignore, might not be registered in editor registry + } + } + } + } + resources.componentTypeNames = []; + } + + // 4. 清理序列化器 + // Step 4: Cleanup serializers + if (this.services && resources.serializerTypes.length > 0) { + const serializerRegistry = this.services.tryResolve(SerializerRegistry); + if (serializerRegistry) { + for (const { pluginName, type } of resources.serializerTypes) { + try { + serializerRegistry.unregister(pluginName, type); + logger.debug(`Serializer unregistered: ${pluginName}/${type}`); + } catch (e) { + logger.error(`Failed to unregister serializer ${pluginName}/${type}:`, e); + } + } + } + resources.serializerTypes = []; + } + + logger.info(`Runtime deactivated for: ${pluginId}`); } /** diff --git a/packages/editor-core/src/Services/UserCode/HotReloadCoordinator.ts b/packages/editor-core/src/Services/UserCode/HotReloadCoordinator.ts new file mode 100644 index 00000000..93703634 --- /dev/null +++ b/packages/editor-core/src/Services/UserCode/HotReloadCoordinator.ts @@ -0,0 +1,328 @@ +/** + * Hot Reload Coordinator + * 热更新协调器 + * + * Coordinates the hot reload process to ensure safe code updates + * without causing race conditions or inconsistent state. + * + * 协调热更新过程,确保代码更新安全,不会导致竞态条件或状态不一致。 + * + * @example + * ```typescript + * const coordinator = new HotReloadCoordinator(); + * await coordinator.performHotReload(async () => { + * // Recompile and reload user code + * await userCodeService.compile(options); + * await userCodeService.load(outputPath, target); + * }); + * ``` + */ + +import { createLogger } from '@esengine/ecs-framework'; +import type { HotReloadEvent } from './IUserCodeService'; + +const logger = createLogger('HotReloadCoordinator'); + +/** + * Hot reload phase enumeration + * 热更新阶段枚举 + */ +export const enum EHotReloadPhase { + /** Idle state, no hot reload in progress | 空闲状态,没有热更新进行中 */ + Idle = 'idle', + /** Preparing for hot reload, pausing systems | 准备热更新,暂停系统 */ + Preparing = 'preparing', + /** Compiling user code | 编译用户代码 */ + Compiling = 'compiling', + /** Loading new modules | 加载新模块 */ + Loading = 'loading', + /** Updating component instances | 更新组件实例 */ + UpdatingInstances = 'updating-instances', + /** Updating systems | 更新系统 */ + UpdatingSystems = 'updating-systems', + /** Resuming systems | 恢复系统 */ + Resuming = 'resuming', + /** Hot reload complete | 热更新完成 */ + Complete = 'complete', + /** Hot reload failed | 热更新失败 */ + Failed = 'failed' +} + +/** + * Hot reload status interface + * 热更新状态接口 + */ +export interface IHotReloadStatus { + /** Current phase | 当前阶段 */ + phase: EHotReloadPhase; + /** Error message if failed | 失败时的错误信息 */ + error?: string; + /** Timestamp when current phase started | 当前阶段开始时间戳 */ + startTime: number; + /** Number of updated instances | 更新的实例数量 */ + updatedInstances?: number; + /** Number of updated systems | 更新的系统数量 */ + updatedSystems?: number; +} + +/** + * Hot reload options + * 热更新选项 + */ +export interface IHotReloadOptions { + /** + * Timeout for hot reload process in milliseconds. + * 热更新过程的超时时间(毫秒)。 + * + * @default 30000 + */ + timeout?: number; + + /** + * Whether to restore previous state on failure. + * 失败时是否恢复到之前的状态。 + * + * @default true + */ + restoreOnFailure?: boolean; + + /** + * Callback for phase changes. + * 阶段变化回调。 + */ + onPhaseChange?: (phase: EHotReloadPhase) => void; +} + +/** + * Hot Reload Coordinator + * 热更新协调器 + * + * Manages the hot reload process lifecycle: + * 1. Pause ECS update loop + * 2. Execute hot reload tasks (compile, load, update) + * 3. Resume ECS update loop + * + * 管理热更新过程生命周期: + * 1. 暂停 ECS 更新循环 + * 2. 执行热更新任务(编译、加载、更新) + * 3. 恢复 ECS 更新循环 + */ +export class HotReloadCoordinator { + private _status: IHotReloadStatus = { + phase: EHotReloadPhase.Idle, + startTime: 0 + }; + + private _coreReference: any = null; + private _previousPausedState: boolean = false; + private _hotReloadPromise: Promise | null = null; + private _onPhaseChange?: (phase: EHotReloadPhase) => void; + + /** + * Get current hot reload status. + * 获取当前热更新状态。 + */ + public get status(): Readonly { + return { ...this._status }; + } + + /** + * Check if hot reload is in progress. + * 检查热更新是否进行中。 + */ + public get isInProgress(): boolean { + return this._status.phase !== EHotReloadPhase.Idle && + this._status.phase !== EHotReloadPhase.Complete && + this._status.phase !== EHotReloadPhase.Failed; + } + + /** + * Initialize coordinator with Core reference. + * 使用 Core 引用初始化协调器。 + * + * @param coreModule - ECS Framework Core module | ECS 框架 Core 模块 + */ + public initialize(coreModule: any): void { + this._coreReference = coreModule; + logger.info('HotReloadCoordinator initialized'); + } + + /** + * Perform a coordinated hot reload. + * 执行协调的热更新。 + * + * This method ensures the ECS loop is paused during hot reload + * and properly resumed afterward, even if an error occurs. + * + * 此方法确保 ECS 循环在热更新期间暂停,并在之后正确恢复,即使发生错误。 + * + * @param reloadTask - Async function that performs the actual reload | 执行实际重载的异步函数 + * @param options - Hot reload options | 热更新选项 + * @returns Promise that resolves when hot reload is complete | 热更新完成时解析的 Promise + */ + public async performHotReload( + reloadTask: () => Promise, + options: IHotReloadOptions = {} + ): Promise { + // Prevent concurrent hot reloads | 防止并发热更新 + if (this._hotReloadPromise) { + logger.warn('Hot reload already in progress, waiting for completion | 热更新已在进行中,等待完成'); + await this._hotReloadPromise; + } + + const { + timeout = 30000, + restoreOnFailure = true, + onPhaseChange + } = options; + + this._onPhaseChange = onPhaseChange; + this._status = { + phase: EHotReloadPhase.Idle, + startTime: Date.now() + }; + + let result: HotReloadEvent | void = undefined; + + this._hotReloadPromise = (async () => { + try { + // Phase 1: Prepare - Pause ECS | 阶段 1:准备 - 暂停 ECS + this._setPhase(EHotReloadPhase.Preparing); + this._pauseECS(); + + // Create timeout promise | 创建超时 Promise + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`Hot reload timed out after ${timeout}ms | 热更新超时 ${timeout}ms`)); + }, timeout); + }); + + // Phase 2-5: Execute reload task with timeout | 阶段 2-5:带超时执行重载任务 + this._setPhase(EHotReloadPhase.Compiling); + result = await Promise.race([ + reloadTask(), + timeoutPromise + ]); + + // Phase 6: Resume ECS | 阶段 6:恢复 ECS + this._setPhase(EHotReloadPhase.Resuming); + this._resumeECS(); + + // Phase 7: Complete | 阶段 7:完成 + this._setPhase(EHotReloadPhase.Complete); + logger.info('Hot reload completed successfully | 热更新成功完成', { + duration: Date.now() - this._status.startTime + }); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this._status.error = errorMessage; + this._setPhase(EHotReloadPhase.Failed); + + logger.error('Hot reload failed | 热更新失败:', error); + + // Always resume ECS on failure | 失败时始终恢复 ECS + if (restoreOnFailure) { + this._resumeECS(); + } + + throw error; + } finally { + this._hotReloadPromise = null; + } + })(); + + await this._hotReloadPromise; + return result; + } + + /** + * Update hot reload status with instance count. + * 更新热更新状态的实例数量。 + * + * @param instanceCount - Number of updated instances | 更新的实例数量 + */ + public reportInstanceUpdate(instanceCount: number): void { + this._status.updatedInstances = instanceCount; + this._setPhase(EHotReloadPhase.UpdatingInstances); + } + + /** + * Update hot reload status with system count. + * 更新热更新状态的系统数量。 + * + * @param systemCount - Number of updated systems | 更新的系统数量 + */ + public reportSystemUpdate(systemCount: number): void { + this._status.updatedSystems = systemCount; + this._setPhase(EHotReloadPhase.UpdatingSystems); + } + + /** + * Pause ECS update loop. + * 暂停 ECS 更新循环。 + */ + private _pauseECS(): void { + if (!this._coreReference) { + logger.warn('Core reference not set, cannot pause ECS | Core 引用未设置,无法暂停 ECS'); + return; + } + + // Store previous paused state to restore later | 存储之前的暂停状态以便后续恢复 + this._previousPausedState = this._coreReference.paused ?? false; + + // Pause ECS | 暂停 ECS + this._coreReference.paused = true; + + logger.debug('ECS paused for hot reload | ECS 已暂停以进行热更新'); + } + + /** + * Resume ECS update loop. + * 恢复 ECS 更新循环。 + */ + private _resumeECS(): void { + if (!this._coreReference) { + logger.warn('Core reference not set, cannot resume ECS | Core 引用未设置,无法恢复 ECS'); + return; + } + + // Restore previous paused state | 恢复之前的暂停状态 + this._coreReference.paused = this._previousPausedState; + + logger.debug('ECS resumed after hot reload | 热更新后 ECS 已恢复', { + paused: this._coreReference.paused + }); + } + + /** + * Set current phase and notify listener. + * 设置当前阶段并通知监听器。 + */ + private _setPhase(phase: EHotReloadPhase): void { + this._status.phase = phase; + + if (this._onPhaseChange) { + try { + this._onPhaseChange(phase); + } catch (error) { + logger.warn('Error in phase change callback | 阶段变化回调错误:', error); + } + } + + logger.debug(`Hot reload phase: ${phase} | 热更新阶段: ${phase}`); + } + + /** + * Reset coordinator state. + * 重置协调器状态。 + */ + public reset(): void { + this._status = { + phase: EHotReloadPhase.Idle, + startTime: 0 + }; + this._hotReloadPromise = null; + this._onPhaseChange = undefined; + } +} diff --git a/packages/editor-core/src/Services/UserCode/IUserCodeService.ts b/packages/editor-core/src/Services/UserCode/IUserCodeService.ts index 86333bbf..79f7189b 100644 --- a/packages/editor-core/src/Services/UserCode/IUserCodeService.ts +++ b/packages/editor-core/src/Services/UserCode/IUserCodeService.ts @@ -11,6 +11,8 @@ * - scripts/editor/ -> Editor-only code (inspectors, gizmos, panels) */ +import type { IHotReloadOptions } from './HotReloadCoordinator'; + /** * User code target environment. * 用户代码目标环境。 @@ -280,8 +282,13 @@ export interface IUserCodeService { * * @param projectPath - Project root path | 项目根路径 * @param onReload - Callback when code is reloaded | 代码重新加载时的回调 + * @param options - Hot reload options | 热更新选项 */ - watch(projectPath: string, onReload: (event: HotReloadEvent) => void): Promise; + watch( + projectPath: string, + onReload: (event: HotReloadEvent) => void, + options?: IHotReloadOptions + ): Promise; /** * Stop watching for file changes. @@ -296,20 +303,36 @@ export interface IUserCodeService { isWatching(): boolean; } +import { EditorConfig } from '../../Config'; + /** * Default scripts directory name. * 默认脚本目录名称。 + * + * @deprecated Use EditorConfig.paths.scripts instead */ -export const SCRIPTS_DIR = 'scripts'; +export const SCRIPTS_DIR = EditorConfig.paths.scripts; /** * Editor scripts subdirectory name. * 编辑器脚本子目录名称。 + * + * @deprecated Use EditorConfig.paths.editorScripts instead */ -export const EDITOR_SCRIPTS_DIR = 'editor'; +export const EDITOR_SCRIPTS_DIR = EditorConfig.paths.editorScripts; /** * Default output directory for compiled user code. * 编译后用户代码的默认输出目录。 + * + * @deprecated Use EditorConfig.paths.compiled instead */ -export const USER_CODE_OUTPUT_DIR = '.esengine/compiled'; +export const USER_CODE_OUTPUT_DIR = EditorConfig.paths.compiled; + +// Re-export hot reload coordinator types +// 重新导出热更新协调器类型 +export { + EHotReloadPhase, + type IHotReloadStatus, + type IHotReloadOptions +} from './HotReloadCoordinator'; diff --git a/packages/editor-core/src/Services/UserCode/UserCodeService.ts b/packages/editor-core/src/Services/UserCode/UserCodeService.ts index 586efff3..e893efd0 100644 --- a/packages/editor-core/src/Services/UserCode/UserCodeService.ts +++ b/packages/editor-core/src/Services/UserCode/UserCodeService.ts @@ -7,7 +7,14 @@ */ import type { IService } from '@esengine/ecs-framework'; -import { Injectable, createLogger, PlatformDetector, ComponentRegistry as CoreComponentRegistry } from '@esengine/ecs-framework'; +import { + Injectable, + createLogger, + PlatformDetector, + ComponentRegistry as CoreComponentRegistry, + COMPONENT_TYPE_NAME, + SYSTEM_TYPE_NAME +} from '@esengine/ecs-framework'; import type { IUserCodeService, UserScriptInfo, @@ -15,7 +22,8 @@ import type { UserCodeCompileResult, CompileError, UserCodeModule, - HotReloadEvent + HotReloadEvent, + IHotReloadOptions } from './IUserCodeService'; import { UserCodeTarget, @@ -23,6 +31,8 @@ import { EDITOR_SCRIPTS_DIR, USER_CODE_OUTPUT_DIR } from './IUserCodeService'; +import { HotReloadCoordinator, EHotReloadPhase } from './HotReloadCoordinator'; +import { EditorConfig } from '../../Config'; import type { IFileSystem, FileEntry } from '../IFileSystem'; import type { ComponentInspectorRegistry, IComponentInspector } from '../ComponentInspectorRegistry'; import { GizmoRegistry } from '../../Gizmos/GizmoRegistry'; @@ -64,8 +74,15 @@ export class UserCodeService implements IService, IUserCodeService { */ private _registeredGizmoTypes: any[] = []; + /** + * 热更新协调器 + * Hot reload coordinator + */ + private _hotReloadCoordinator: HotReloadCoordinator; + constructor(fileSystem: IFileSystem) { this._fileSystem = fileSystem; + this._hotReloadCoordinator = new HotReloadCoordinator(); } /** @@ -160,8 +177,8 @@ export class UserCodeService implements IService, IUserCodeService { // Determine output file name | 确定输出文件名 const outputFileName = options.target === UserCodeTarget.Runtime - ? 'user-runtime.js' - : 'user-editor.js'; + ? EditorConfig.output.runtimeBundle + : EditorConfig.output.editorBundle; const outputPath = `${outputDir}${sep}${outputFileName}`; // Build entry point content | 构建入口点内容 @@ -176,8 +193,8 @@ export class UserCodeService implements IService, IUserCodeService { // Determine global name for IIFE output | 确定 IIFE 输出的全局名称 const globalName = options.target === UserCodeTarget.Runtime - ? '__USER_RUNTIME_EXPORTS__' - : '__USER_EDITOR_EXPORTS__'; + ? EditorConfig.globals.userRuntimeExports + : EditorConfig.globals.userEditorExports; // Build alias map for framework dependencies | 构建框架依赖的别名映射 const shimPath = `${outputDir}${sep}_shim_ecs_framework.js`.replace(/\\/g, '/'); @@ -421,7 +438,8 @@ export class UserCodeService implements IService, IUserCodeService { // Access scene through Core.scene // 通过 Core.scene 访问场景 - const Core = (window as any).__ESENGINE__?.ecsFramework?.Core; + const sdkGlobal = (window as any)[EditorConfig.globals.sdk]; + const Core = sdkGlobal?.ecsFramework?.Core; const scene = Core?.scene; if (!scene || !scene.entities) { logger.warn('No active scene for hot reload | 没有活动场景用于热更新'); @@ -526,9 +544,10 @@ export class UserCodeService implements IService, IUserCodeService { systemInstance.enabled = enabled; } - // 标记为用户系统,便于后续识别和移除 | Mark as user system for later identification and removal - systemInstance.__isUserSystem__ = true; - systemInstance.__userSystemName__ = name; + // 标记为用户系统,便于后续识别和移除 + // Mark as user system for later identification and removal + systemInstance[EditorConfig.typeMarkers.userSystem] = true; + systemInstance[EditorConfig.typeMarkers.userSystemName] = name; // 添加到场景 | Add to scene scene.addSystem(systemInstance); @@ -565,9 +584,11 @@ export class UserCodeService implements IService, IUserCodeService { for (const system of this._registeredSystems) { try { scene.removeSystem(system); - logger.debug(`Removed user system: ${system.__userSystemName__} | 移除用户系统: ${system.__userSystemName__}`); + const systemName = system[EditorConfig.typeMarkers.userSystemName]; + logger.debug(`Removed user system: ${systemName} | 移除用户系统: ${systemName}`); } catch (err) { - logger.warn(`Failed to remove system ${system.__userSystemName__}:`, err); + const systemName = system[EditorConfig.typeMarkers.userSystemName]; + logger.warn(`Failed to remove system ${systemName}:`, err); } } @@ -692,8 +713,13 @@ export class UserCodeService implements IService, IUserCodeService { * * @param projectPath - Project root path | 项目根路径 * @param onReload - Callback when code is reloaded | 代码重新加载时的回调 + * @param options - Hot reload options | 热更新选项 */ - async watch(projectPath: string, onReload: (event: HotReloadEvent) => void): Promise { + async watch( + projectPath: string, + onReload: (event: HotReloadEvent) => void, + options?: IHotReloadOptions + ): Promise { if (this._watching) { this._watchCallbacks.push(onReload); return; @@ -701,6 +727,16 @@ export class UserCodeService implements IService, IUserCodeService { this._currentProjectPath = projectPath; + // Initialize hot reload coordinator with Core reference + // 使用 Core 引用初始化热更新协调器 + const sdkGlobal = (window as any)[EditorConfig.globals.sdk]; + const Core = sdkGlobal?.ecsFramework?.Core; + if (Core) { + this._hotReloadCoordinator.initialize(Core); + } else { + logger.warn('Core not available for HotReloadCoordinator | Core 不可用于热更新协调器'); + } + try { // Check if we're in Tauri environment | 检查是否在 Tauri 环境 if (PlatformDetector.isTauriEnvironment()) { @@ -731,27 +767,44 @@ export class UserCodeService implements IService, IUserCodeService { // Get previous module | 获取之前的模块 const previousModule = this.getModule(target); - // Recompile the affected target | 重新编译受影响的目标 - const compileResult = await this.compile({ - projectPath, - target - }); + // Use coordinator for synchronized hot reload + // 使用协调器进行同步热更新 + try { + const hotReloadEvent = await this._hotReloadCoordinator.performHotReload( + async () => { + // Recompile the affected target | 重新编译受影响的目标 + const compileResult = await this.compile({ + projectPath, + target + }); - if (compileResult.success && compileResult.outputPath) { - // Reload the module | 重新加载模块 - const newModule = await this.load(compileResult.outputPath, target); + if (!compileResult.success || !compileResult.outputPath) { + throw new Error( + `Hot reload compilation failed: ${compileResult.errors.map(e => e.message).join(', ')}` + ); + } - // Create hot reload event | 创建热更新事件 - const hotReloadEvent: HotReloadEvent = { - target, - changedFiles: paths, - previousModule, - newModule - }; + // Reload the module | 重新加载模块 + const newModule = await this.load(compileResult.outputPath, target); - this._notifyHotReload(hotReloadEvent); - } else { - logger.error('Hot reload compilation failed | 热更新编译失败', compileResult.errors); + // Create hot reload event | 创建热更新事件 + const reloadEvent: HotReloadEvent = { + target, + changedFiles: paths, + previousModule, + newModule + }; + + return reloadEvent; + }, + options + ) as HotReloadEvent; + + if (hotReloadEvent) { + this._notifyHotReload(hotReloadEvent); + } + } catch (error) { + logger.error('Hot reload failed | 热更新失败:', error); } }); @@ -774,6 +827,16 @@ export class UserCodeService implements IService, IUserCodeService { } } + /** + * Get the hot reload coordinator. + * 获取热更新协调器。 + * + * @returns Hot reload coordinator instance | 热更新协调器实例 + */ + getHotReloadCoordinator(): HotReloadCoordinator { + return this._hotReloadCoordinator; + } + /** * Stop watching for file changes. * 停止监视文件变更。 @@ -971,12 +1034,13 @@ export class UserCodeService implements IService, IUserCodeService { const shimPaths: string[] = []; // Create shim for @esengine/ecs-framework | 为 @esengine/ecs-framework 创建 shim - // This uses window.__ESENGINE__.ecsFramework set by PluginSDKRegistry - // 这使用 PluginSDKRegistry 设置的 window.__ESENGINE__.ecsFramework + // This uses window[EditorConfig.globals.sdk].ecsFramework set by PluginSDKRegistry + // 这使用 PluginSDKRegistry 设置的 window[EditorConfig.globals.sdk].ecsFramework const ecsShimPath = `${outputDir}${sep}_shim_ecs_framework.js`; + const sdkGlobalName = EditorConfig.globals.sdk; const ecsShimContent = `// Shim for @esengine/ecs-framework -// Maps to window.__ESENGINE__.ecsFramework set by PluginSDKRegistry -module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window.__ESENGINE__.ecsFramework) || {}; +// Maps to window.${sdkGlobalName}.ecsFramework set by PluginSDKRegistry +module.exports = (typeof window !== 'undefined' && window.${sdkGlobalName} && window.${sdkGlobalName}.ecsFramework) || {}; `; await this._fileSystem.writeFile(ecsShimPath, ecsShimContent); shimPaths.push(ecsShimPath); @@ -1101,8 +1165,8 @@ module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window ): Promise> { // Determine global name based on target | 根据目标确定全局名称 const globalName = target === UserCodeTarget.Runtime - ? '__USER_RUNTIME_EXPORTS__' - : '__USER_EDITOR_EXPORTS__'; + ? EditorConfig.globals.userRuntimeExports + : EditorConfig.globals.userEditorExports; // Clear any previous exports | 清除之前的导出 (window as any)[globalName] = undefined; @@ -1150,47 +1214,45 @@ module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window } /** - * Check if a class extends Component. - * 检查类是否继承自 Component。 + * Check if a class is decorated with @ECSComponent. + * 检查类是否使用了 @ECSComponent 装饰器。 * - * Uses the actual Component class from the global framework to check inheritance. - * 使用全局框架中的实际 Component 类来检查继承关系。 + * 用户组件必须使用 @ECSComponent 装饰器才能被识别。 + * User components must use @ECSComponent decorator to be recognized. + * + * @example + * ```typescript + * @ECSComponent('MyComponent') + * class MyComponent extends Component { + * // ... + * } + * ``` */ private _isComponentClass(cls: any): boolean { - // Get Component class from global framework | 从全局框架获取 Component 类 - const framework = (window as any).__ESENGINE__?.ecsFramework; - - if (!framework?.Component) { - return false; - } - - // Use instanceof or prototype chain check | 使用 instanceof 或原型链检查 - try { - const ComponentClass = framework.Component; - - // Check if cls.prototype is an instance of Component - // 检查 cls.prototype 是否是 Component 的实例 - return cls.prototype instanceof ComponentClass || - ComponentClass.prototype.isPrototypeOf(cls.prototype); - } catch { - return false; - } + // 检查是否有 @ECSComponent 装饰器(通过 Symbol 键) + // Check if class has @ECSComponent decorator (via Symbol key) + return cls?.[COMPONENT_TYPE_NAME] !== undefined; } /** - * Check if a class extends System. - * 检查类是否继承自 System。 + * Check if a class is decorated with @ECSSystem. + * 检查类是否使用了 @ECSSystem 装饰器。 + * + * 用户系统必须使用 @ECSSystem 装饰器才能被识别。 + * User systems must use @ECSSystem decorator to be recognized. + * + * @example + * ```typescript + * @ECSSystem('MySystem') + * class MySystem extends EntitySystem { + * // ... + * } + * ``` */ private _isSystemClass(cls: any): boolean { - let proto = cls.prototype; - while (proto) { - const name = proto.constructor?.name; - if (name === 'System' || name === 'EntityProcessingSystem') { - return true; - } - proto = Object.getPrototypeOf(proto); - } - return false; + // 检查是否有 @ECSSystem 装饰器(通过 Symbol 键) + // Check if class has @ECSSystem decorator (via Symbol key) + return cls?.[SYSTEM_TYPE_NAME] !== undefined; } /** diff --git a/packages/editor-core/src/Services/UserCode/index.ts b/packages/editor-core/src/Services/UserCode/index.ts index 141149ff..183c4ec3 100644 --- a/packages/editor-core/src/Services/UserCode/index.ts +++ b/packages/editor-core/src/Services/UserCode/index.ts @@ -102,14 +102,19 @@ export type { UserCodeCompileResult, CompileError, UserCodeModule, - HotReloadEvent + HotReloadEvent, + IHotReloadStatus, + IHotReloadOptions } from './IUserCodeService'; export { UserCodeTarget, SCRIPTS_DIR, EDITOR_SCRIPTS_DIR, - USER_CODE_OUTPUT_DIR + USER_CODE_OUTPUT_DIR, + EHotReloadPhase } from './IUserCodeService'; export { UserCodeService } from './UserCodeService'; + +export { HotReloadCoordinator } from './HotReloadCoordinator'; diff --git a/packages/editor-core/src/index.ts b/packages/editor-core/src/index.ts index 42cdabb4..ea3a500c 100644 --- a/packages/editor-core/src/index.ts +++ b/packages/editor-core/src/index.ts @@ -4,6 +4,9 @@ * Plugin-based editor framework for ECS Framework */ +// 配置 | Configuration +export * from './Config'; + // 新插件系统 | New plugin system export * from './Plugin';