refactor(editor): 统一配置管理、完善插件卸载和热更新同步 (#298)
主要变更: 1. 统一配置管理 (EditorConfig) - 新增 EditorConfig 集中管理路径、文件名、全局变量等配置 - 添加 SDK 模块配置系统 (ISDKModuleConfig) - 重构 PluginSDKRegistry 使用配置而非硬编码 2. 完善插件卸载机制 - 扩展 PluginRegisteredResources 追踪运行时资源 - 实现完整的 deactivatePluginRuntime 清理流程 - ComponentRegistry 添加 unregister/getRegisteredComponents 方法 3. 热更新同步机制 - 新增 HotReloadCoordinator 协调热更新过程 - 热更新期间暂停 ECS 循环避免竞态条件 - 支持超时保护和失败恢复
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
397
packages/editor-core/src/Config/EditorConfig.ts
Normal file
397
packages/editor-core/src/Config/EditorConfig.ts
Normal 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);
|
||||
}
|
||||
5
packages/editor-core/src/Config/index.ts
Normal file
5
packages/editor-core/src/Config/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 配置模块导出
|
||||
* Configuration module exports
|
||||
*/
|
||||
export * from './EditorConfig';
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
* Plugin-based editor framework for ECS Framework
|
||||
*/
|
||||
|
||||
// 配置 | Configuration
|
||||
export * from './Config';
|
||||
|
||||
// 新插件系统 | New plugin system
|
||||
export * from './Plugin';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user