refactor(editor): 统一配置管理、完善插件卸载和热更新同步 (#298)

主要变更:

1. 统一配置管理 (EditorConfig)
   - 新增 EditorConfig 集中管理路径、文件名、全局变量等配置
   - 添加 SDK 模块配置系统 (ISDKModuleConfig)
   - 重构 PluginSDKRegistry 使用配置而非硬编码

2. 完善插件卸载机制
   - 扩展 PluginRegisteredResources 追踪运行时资源
   - 实现完整的 deactivatePluginRuntime 清理流程
   - ComponentRegistry 添加 unregister/getRegisteredComponents 方法

3. 热更新同步机制
   - 新增 HotReloadCoordinator 协调热更新过程
   - 热更新期间暂停 ECS 循环避免竞态条件
   - 支持超时保护和失败恢复
This commit is contained in:
YHH
2025-12-09 09:06:29 +08:00
committed by GitHub
parent 40a38b8b88
commit 6c99b811ec
12 changed files with 1270 additions and 172 deletions

View File

@@ -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;

View File

@@ -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();

View File

@@ -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<string, LoadedPluginMeta> = 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<IPlugin | null> {
const pluginKey = this.sanitizePluginKey(pluginName);
const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record<string, any>;
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<void> {
const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record<string, any> | 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<string, any>;
}
}

View File

@@ -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<string, any> = {
'@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<T>(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: <T>(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<string, string> {
return { ...SDK_GLOBALS };
return getSDKGlobalsMapping();
}
}
// 重新导出辅助函数,方便插件构建工具使用
// Re-export helper functions for plugin build tools
export { getSDKGlobalsMapping, getSDKPackageNames, getEnabledSDKModules };

View File

@@ -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<string, string> {
const sdkGlobalName = EditorConfig.globals.sdk;
const mapping: Record<string, string> = {};
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);
}

View File

@@ -0,0 +1,5 @@
/**
* 配置模块导出
* Configuration module exports
*/
export * from './EditorConfig';

View File

@@ -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}`);
}
/**

View File

@@ -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<void> | null = null;
private _onPhaseChange?: (phase: EHotReloadPhase) => void;
/**
* Get current hot reload status.
* 获取当前热更新状态。
*/
public get status(): Readonly<IHotReloadStatus> {
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<HotReloadEvent | void>,
options: IHotReloadOptions = {}
): Promise<HotReloadEvent | void> {
// 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<never>((_, 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;
}
}

View File

@@ -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<void>;
watch(
projectPath: string,
onReload: (event: HotReloadEvent) => void,
options?: IHotReloadOptions
): Promise<void>;
/**
* 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';

View File

@@ -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<void> {
async watch(
projectPath: string,
onReload: (event: HotReloadEvent) => void,
options?: IHotReloadOptions
): Promise<void> {
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<Record<string, any>> {
// 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;
}
/**

View File

@@ -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';

View File

@@ -4,6 +4,9 @@
* Plugin-based editor framework for ECS Framework
*/
// 配置 | Configuration
export * from './Config';
// 新插件系统 | New plugin system
export * from './Plugin';