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生成器
|
||||||
*
|
*
|
||||||
* 用于为每个组件分配唯一的ID。
|
* 用于为每个组件分配唯一的ID。
|
||||||
|
*
|
||||||
|
* Component ID generator.
|
||||||
|
* Used to assign unique IDs to each component.
|
||||||
*/
|
*/
|
||||||
private static idGenerator: number = 0;
|
private static idGenerator: number = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -259,8 +259,64 @@ export class ComponentRegistry {
|
|||||||
return this.hotReloadEnabled;
|
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 {
|
public static reset(): void {
|
||||||
this.componentTypes.clear();
|
this.componentTypes.clear();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Project Plugin Loader
|
* 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 type { IPlugin, ModuleManifest } from '@esengine/editor-core';
|
||||||
import { Core } from '@esengine/ecs-framework';
|
import { Core } from '@esengine/ecs-framework';
|
||||||
import { TauriAPI } from '../api/tauri';
|
import { TauriAPI } from '../api/tauri';
|
||||||
@@ -30,13 +30,19 @@ interface LoadedPluginMeta {
|
|||||||
scriptElement?: HTMLScriptElement;
|
scriptElement?: HTMLScriptElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件容器全局变量名
|
||||||
|
* Plugin container global variable name
|
||||||
|
*/
|
||||||
|
const PLUGINS_GLOBAL_NAME = EditorConfig.globals.plugins;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目插件加载器
|
* 项目插件加载器
|
||||||
*
|
*
|
||||||
* 使用全局变量方案加载插件:
|
* 使用全局变量方案加载插件:
|
||||||
* 1. 插件构建时将 @esengine/* 标记为 external
|
* 1. 插件构建时将 @esengine/* 标记为 external
|
||||||
* 2. 插件输出为 IIFE 格式,依赖从 window.__ESENGINE__ 获取
|
* 2. 插件输出为 IIFE 格式,依赖从全局 SDK 对象获取
|
||||||
* 3. 插件导出到 window.__ESENGINE_PLUGINS__[pluginName]
|
* 3. 插件导出到全局插件容器
|
||||||
*/
|
*/
|
||||||
export class PluginLoader {
|
export class PluginLoader {
|
||||||
private loadedPlugins: Map<string, LoadedPluginMeta> = new Map();
|
private loadedPlugins: Map<string, LoadedPluginMeta> = new Map();
|
||||||
@@ -51,7 +57,7 @@ export class PluginLoader {
|
|||||||
// 初始化插件容器
|
// 初始化插件容器
|
||||||
this.initPluginContainer();
|
this.initPluginContainer();
|
||||||
|
|
||||||
const pluginsPath = `${projectPath}/plugins`;
|
const pluginsPath = getPluginsPath(projectPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exists = await TauriAPI.pathExists(pluginsPath);
|
const exists = await TauriAPI.pathExists(pluginsPath);
|
||||||
@@ -75,8 +81,8 @@ export class PluginLoader {
|
|||||||
* 初始化插件容器
|
* 初始化插件容器
|
||||||
*/
|
*/
|
||||||
private initPluginContainer(): void {
|
private initPluginContainer(): void {
|
||||||
if (!window.__ESENGINE_PLUGINS__) {
|
if (!(window as any)[PLUGINS_GLOBAL_NAME]) {
|
||||||
window.__ESENGINE_PLUGINS__ = {};
|
(window as any)[PLUGINS_GLOBAL_NAME] = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,25 +167,27 @@ export class PluginLoader {
|
|||||||
): Promise<IPlugin | null> {
|
): Promise<IPlugin | null> {
|
||||||
const pluginKey = this.sanitizePluginKey(pluginName);
|
const pluginKey = this.sanitizePluginKey(pluginName);
|
||||||
|
|
||||||
|
const pluginsContainer = (window as any)[PLUGINS_GLOBAL_NAME] as Record<string, any>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 插件代码是 IIFE 格式,会自动导出到 window.__ESENGINE_PLUGINS__
|
// 插件代码是 IIFE 格式,会自动导出到全局插件容器
|
||||||
await this.executeViaScriptTag(code, pluginName);
|
await this.executeViaScriptTag(code, pluginName);
|
||||||
|
|
||||||
// 从全局容器获取插件模块
|
// 从全局容器获取插件模块
|
||||||
const pluginModule = window.__ESENGINE_PLUGINS__[pluginKey];
|
const pluginModule = pluginsContainer[pluginKey];
|
||||||
if (!pluginModule) {
|
if (!pluginModule) {
|
||||||
// 尝试其他可能的 key 格式
|
// 尝试其他可能的 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, '_'))
|
k.includes(pluginName.replace(/@/g, '').replace(/\//g, '_').replace(/-/g, '_'))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (altKeys.length > 0 && altKeys[0] !== undefined) {
|
if (altKeys.length > 0 && altKeys[0] !== undefined) {
|
||||||
const foundKey = altKeys[0];
|
const foundKey = altKeys[0];
|
||||||
const altModule = window.__ESENGINE_PLUGINS__[foundKey];
|
const altModule = pluginsContainer[foundKey];
|
||||||
return this.findPluginLoader(altModule);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,11 +338,13 @@ export class PluginLoader {
|
|||||||
* 卸载所有已加载的插件
|
* 卸载所有已加载的插件
|
||||||
*/
|
*/
|
||||||
async unloadProjectPlugins(_pluginManager: PluginManager): Promise<void> {
|
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()) {
|
for (const pluginName of this.loadedPlugins.keys()) {
|
||||||
// 清理全局容器中的插件
|
// 清理全局容器中的插件
|
||||||
const pluginKey = this.sanitizePluginKey(pluginName);
|
const pluginKey = this.sanitizePluginKey(pluginName);
|
||||||
if (window.__ESENGINE_PLUGINS__?.[pluginKey]) {
|
if (pluginsContainer?.[pluginKey]) {
|
||||||
delete window.__ESENGINE_PLUGINS__[pluginKey];
|
delete pluginsContainer[pluginKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除 script 标签
|
// 移除 script 标签
|
||||||
@@ -353,10 +363,3 @@ export class PluginLoader {
|
|||||||
return Array.from(this.loadedPlugins.keys());
|
return Array.from(this.loadedPlugins.keys());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局类型声明
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
__ESENGINE_PLUGINS__: Record<string, any>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,96 +5,122 @@
|
|||||||
* 将编辑器核心模块暴露为全局变量,供插件使用。
|
* 将编辑器核心模块暴露为全局变量,供插件使用。
|
||||||
* 插件构建时将这些模块标记为 external,运行时从全局对象获取。
|
* 插件构建时将这些模块标记为 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()
|
* 1. 编辑器启动时调用 PluginSDKRegistry.initialize()
|
||||||
* 2. 插件构建配置中设置 external: ['@esengine/editor-runtime', ...]
|
* 2. 插件构建配置中设置 external: getSDKPackageNames()
|
||||||
* 3. 插件构建配置中设置 globals: { '@esengine/editor-runtime': '__ESENGINE__.editorRuntime' }
|
* 3. 插件构建配置中设置 globals: getSDKGlobalsMapping()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Core } from '@esengine/ecs-framework';
|
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';
|
||||||
|
|
||||||
// 导入所有需要暴露给插件的模块
|
// 动态导入所有 SDK 模块
|
||||||
import * as editorRuntime from '@esengine/editor-runtime';
|
// Dynamic import all SDK modules
|
||||||
import * as ecsFramework from '@esengine/ecs-framework';
|
import * as ecsFramework from '@esengine/ecs-framework';
|
||||||
|
import * as editorRuntime from '@esengine/editor-runtime';
|
||||||
import * as behaviorTree from '@esengine/behavior-tree';
|
import * as behaviorTree from '@esengine/behavior-tree';
|
||||||
import * as engineCore from '@esengine/engine-core';
|
import * as engineCore from '@esengine/engine-core';
|
||||||
import * as sprite from '@esengine/sprite';
|
import * as sprite from '@esengine/sprite';
|
||||||
import * as camera from '@esengine/camera';
|
import * as camera from '@esengine/camera';
|
||||||
import * as audio from '@esengine/audio';
|
import * as audio from '@esengine/audio';
|
||||||
|
|
||||||
// 存储服务实例引用(在初始化时设置)
|
/**
|
||||||
let entityStoreInstance: EntityStoreService | null = null;
|
* 模块实例映射
|
||||||
let messageHubInstance: MessageHub | null = null;
|
* Module instance mapping
|
||||||
|
*
|
||||||
// SDK 模块映射
|
* 由于 ES 模块的静态导入限制,我们需要维护一个包名到模块的映射。
|
||||||
const SDK_MODULES = {
|
* Due to ES module static import limitations, we need to maintain a mapping from package name to module.
|
||||||
'@esengine/editor-runtime': editorRuntime,
|
*/
|
||||||
|
const MODULE_INSTANCES: Record<string, any> = {
|
||||||
'@esengine/ecs-framework': ecsFramework,
|
'@esengine/ecs-framework': ecsFramework,
|
||||||
|
'@esengine/editor-runtime': editorRuntime,
|
||||||
'@esengine/behavior-tree': behaviorTree,
|
'@esengine/behavior-tree': behaviorTree,
|
||||||
'@esengine/engine-core': engineCore,
|
'@esengine/engine-core': engineCore,
|
||||||
'@esengine/sprite': sprite,
|
'@esengine/sprite': sprite,
|
||||||
'@esengine/camera': camera,
|
'@esengine/camera': camera,
|
||||||
'@esengine/audio': audio,
|
'@esengine/audio': audio,
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
// 全局变量名称映射(用于插件构建配置)
|
// 存储服务实例引用(在初始化时设置)
|
||||||
export const SDK_GLOBALS = {
|
// Service instance references (set during initialization)
|
||||||
'@esengine/editor-runtime': '__ESENGINE__.editorRuntime',
|
let entityStoreInstance: EntityStoreService | null = null;
|
||||||
'@esengine/ecs-framework': '__ESENGINE__.ecsFramework',
|
let messageHubInstance: MessageHub | null = null;
|
||||||
'@esengine/behavior-tree': '__ESENGINE__.behaviorTree',
|
|
||||||
'@esengine/engine-core': '__ESENGINE__.engineCore',
|
|
||||||
'@esengine/sprite': '__ESENGINE__.sprite',
|
|
||||||
'@esengine/camera': '__ESENGINE__.camera',
|
|
||||||
'@esengine/audio': '__ESENGINE__.audio',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件 API 接口
|
* 插件 API 接口
|
||||||
* 为插件提供统一的访问接口,避免模块实例不一致的问题
|
* Plugin API interface
|
||||||
|
*
|
||||||
|
* 为插件提供统一的访问接口,避免模块实例不一致的问题。
|
||||||
|
* Provides unified access interface for plugins, avoiding module instance inconsistency issues.
|
||||||
*/
|
*/
|
||||||
export interface IPluginAPI {
|
export interface IPluginAPI {
|
||||||
/** 获取当前场景 */
|
/** 获取当前场景 | Get current scene */
|
||||||
getScene(): any;
|
getScene(): any;
|
||||||
/** 获取 EntityStoreService */
|
/** 获取 EntityStoreService | Get EntityStoreService */
|
||||||
getEntityStore(): EntityStoreService;
|
getEntityStore(): EntityStoreService;
|
||||||
/** 获取 MessageHub */
|
/** 获取 MessageHub | Get MessageHub */
|
||||||
getMessageHub(): MessageHub;
|
getMessageHub(): MessageHub;
|
||||||
/** 解析服务 */
|
/** 解析服务 | Resolve service */
|
||||||
resolveService<T>(serviceType: any): T;
|
resolveService<T>(serviceType: any): T;
|
||||||
/** 获取 Core 实例 */
|
/** 获取 Core 实例 | Get Core instance */
|
||||||
getCore(): typeof Core;
|
getCore(): typeof Core;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 扩展 Window.__ESENGINE__ 类型(基础类型已在 PluginAPI.ts 中定义)
|
/**
|
||||||
interface ESEngineGlobal {
|
* SDK 全局对象类型
|
||||||
editorRuntime: typeof editorRuntime;
|
* SDK global object type
|
||||||
ecsFramework: typeof ecsFramework;
|
*/
|
||||||
behaviorTree: typeof behaviorTree;
|
export interface ISDKGlobal {
|
||||||
engineCore: typeof engineCore;
|
/** 动态模块加载 | Dynamic module loading */
|
||||||
sprite: typeof sprite;
|
|
||||||
camera: typeof camera;
|
|
||||||
audio: typeof audio;
|
|
||||||
require: (moduleName: string) => any;
|
require: (moduleName: string) => any;
|
||||||
|
/** 插件 API | Plugin API */
|
||||||
api: IPluginAPI;
|
api: IPluginAPI;
|
||||||
|
/** 其他动态注册的模块 | Other dynamically registered modules */
|
||||||
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件 SDK 注册器
|
* 插件 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 {
|
export class PluginSDKRegistry {
|
||||||
private static initialized = false;
|
private static initialized = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化 SDK 注册器
|
* 初始化 SDK 注册器
|
||||||
* 将所有 SDK 模块暴露到全局对象
|
* Initialize SDK registry
|
||||||
|
*
|
||||||
|
* 将所有配置的 SDK 模块暴露到全局对象。
|
||||||
|
* Exposes all configured SDK modules to global object.
|
||||||
*/
|
*/
|
||||||
static initialize(): void {
|
static initialize(): void {
|
||||||
if (this.initialized) {
|
if (this.initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取服务实例(使用编辑器内部的类型,确保类型匹配)
|
// 获取服务实例
|
||||||
|
// Get service instances
|
||||||
entityStoreInstance = Core.services.resolve(EntityStoreService);
|
entityStoreInstance = Core.services.resolve(EntityStoreService);
|
||||||
messageHubInstance = Core.services.resolve(MessageHub);
|
messageHubInstance = Core.services.resolve(MessageHub);
|
||||||
|
|
||||||
@@ -105,8 +131,47 @@ export class PluginSDKRegistry {
|
|||||||
console.error('[PluginSDKRegistry] MessageHub not registered yet!');
|
console.error('[PluginSDKRegistry] MessageHub not registered yet!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建插件 API - 直接返回实例引用,避免类型匹配问题
|
// 创建 SDK 全局对象
|
||||||
const pluginAPI: IPluginAPI = {
|
// 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,
|
getScene: () => Core.scene,
|
||||||
getEntityStore: () => {
|
getEntityStore: () => {
|
||||||
if (!entityStoreInstance) {
|
if (!entityStoreInstance) {
|
||||||
@@ -123,36 +188,29 @@ export class PluginSDKRegistry {
|
|||||||
resolveService: <T>(serviceType: any): T => Core.services.resolve(serviceType) as T,
|
resolveService: <T>(serviceType: any): T => Core.services.resolve(serviceType) as T,
|
||||||
getCore: () => Core,
|
getCore: () => Core,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建全局命名空间
|
|
||||||
window.__ESENGINE__ = {
|
|
||||||
editorRuntime,
|
|
||||||
ecsFramework,
|
|
||||||
behaviorTree,
|
|
||||||
engineCore,
|
|
||||||
sprite,
|
|
||||||
camera,
|
|
||||||
audio,
|
|
||||||
require: this.requireModule.bind(this),
|
|
||||||
api: pluginAPI,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态获取模块(用于 CommonJS 风格的插件)
|
* 动态获取模块(用于 CommonJS 风格的插件)
|
||||||
|
* Dynamic module loading (for CommonJS style plugins)
|
||||||
|
*
|
||||||
|
* @param moduleName 模块包名 | Module package name
|
||||||
*/
|
*/
|
||||||
private static requireModule(moduleName: string): any {
|
private static requireModule(moduleName: string): any {
|
||||||
const module = SDK_MODULES[moduleName as keyof typeof SDK_MODULES];
|
const module = MODULE_INSTANCES[moduleName];
|
||||||
if (!module) {
|
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;
|
return module;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否已初始化
|
* 检查是否已初始化
|
||||||
|
* Check if initialized
|
||||||
*/
|
*/
|
||||||
static isInitialized(): boolean {
|
static isInitialized(): boolean {
|
||||||
return this.initialized;
|
return this.initialized;
|
||||||
@@ -160,15 +218,25 @@ export class PluginSDKRegistry {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有可用的 SDK 模块名称
|
* 获取所有可用的 SDK 模块名称
|
||||||
|
* Get all available SDK module names
|
||||||
|
*
|
||||||
|
* @deprecated 使用 getSDKPackageNames() 代替 | Use getSDKPackageNames() instead
|
||||||
*/
|
*/
|
||||||
static getAvailableModules(): string[] {
|
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> {
|
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 { UIRegistry } from '../Services/UIRegistry';
|
||||||
import { MessageHub } from '../Services/MessageHub';
|
import { MessageHub } from '../Services/MessageHub';
|
||||||
import { moduleRegistry } from '../Services/Module/ModuleRegistry';
|
import { moduleRegistry } from '../Services/Module/ModuleRegistry';
|
||||||
|
import { SerializerRegistry } from '../Services/SerializerRegistry';
|
||||||
|
import { ComponentRegistry as EditorComponentRegistry } from '../Services/ComponentRegistry';
|
||||||
|
|
||||||
const logger = createLogger('PluginManager');
|
const logger = createLogger('PluginManager');
|
||||||
|
|
||||||
@@ -71,6 +73,9 @@ export interface NormalizedPlugin {
|
|||||||
* Resources registered by plugin (for cleanup on unload)
|
* Resources registered by plugin (for cleanup on unload)
|
||||||
*/
|
*/
|
||||||
export interface PluginRegisteredResources {
|
export interface PluginRegisteredResources {
|
||||||
|
// ==================== 编辑器资源 ====================
|
||||||
|
// Editor resources
|
||||||
|
|
||||||
/** 注册的面板ID | Registered panel IDs */
|
/** 注册的面板ID | Registered panel IDs */
|
||||||
panelIds: string[];
|
panelIds: string[];
|
||||||
/** 注册的菜单ID | Registered menu IDs */
|
/** 注册的菜单ID | Registered menu IDs */
|
||||||
@@ -85,6 +90,16 @@ export interface PluginRegisteredResources {
|
|||||||
fileHandlers: any[];
|
fileHandlers: any[];
|
||||||
/** 注册的文件模板 | Registered file templates */
|
/** 注册的文件模板 | Registered file templates */
|
||||||
fileTemplates: any[];
|
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}`);
|
logger.info(`activatePluginEditor: activating ${pluginId}`);
|
||||||
|
|
||||||
// 初始化资源跟踪
|
// 初始化资源跟踪
|
||||||
|
// Initialize resource tracking
|
||||||
const resources: PluginRegisteredResources = {
|
const resources: PluginRegisteredResources = {
|
||||||
|
// 编辑器资源 | Editor resources
|
||||||
panelIds: [],
|
panelIds: [],
|
||||||
menuIds: [],
|
menuIds: [],
|
||||||
toolbarIds: [],
|
toolbarIds: [],
|
||||||
entityTemplateIds: [],
|
entityTemplateIds: [],
|
||||||
componentActions: [],
|
componentActions: [],
|
||||||
fileHandlers: [],
|
fileHandlers: [],
|
||||||
fileTemplates: []
|
fileTemplates: [],
|
||||||
|
// 运行时资源 | Runtime resources
|
||||||
|
componentTypeNames: [],
|
||||||
|
systemInstances: [],
|
||||||
|
serializerTypes: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取注册表服务
|
// 获取注册表服务
|
||||||
@@ -627,31 +648,73 @@ export class PluginManager implements IService {
|
|||||||
const runtimeModule = plugin.plugin.runtimeModule;
|
const runtimeModule = plugin.plugin.runtimeModule;
|
||||||
if (!runtimeModule) return;
|
if (!runtimeModule) return;
|
||||||
|
|
||||||
// 注册组件
|
// 确保资源跟踪对象存在
|
||||||
if (runtimeModule.registerComponents) {
|
// Ensure resource tracking object exists
|
||||||
runtimeModule.registerComponents(ComponentRegistry);
|
if (!plugin.registeredResources) {
|
||||||
logger.debug(`Components registered for: ${pluginId}`);
|
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) {
|
if (runtimeModule.registerServices) {
|
||||||
runtimeModule.registerServices(this.services);
|
runtimeModule.registerServices(this.services);
|
||||||
logger.debug(`Services registered for: ${pluginId}`);
|
logger.debug(`Services registered for: ${pluginId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建系统
|
// 创建系统(跟踪创建的系统)
|
||||||
|
// Create systems (track created systems)
|
||||||
if (runtimeModule.createSystems) {
|
if (runtimeModule.createSystems) {
|
||||||
|
const systemsBefore = this.currentScene.systems.length;
|
||||||
runtimeModule.createSystems(this.currentScene, this.currentContext);
|
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) {
|
if (runtimeModule.onSystemsCreated) {
|
||||||
runtimeModule.onSystemsCreated(this.currentScene, this.currentContext);
|
runtimeModule.onSystemsCreated(this.currentScene, this.currentContext);
|
||||||
logger.debug(`Systems wired for: ${pluginId}`);
|
logger.debug(`Systems wired for: ${pluginId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用初始化
|
// 调用初始化
|
||||||
|
// Call initialization
|
||||||
if (runtimeModule.onInitialize) {
|
if (runtimeModule.onInitialize) {
|
||||||
await runtimeModule.onInitialize();
|
await runtimeModule.onInitialize();
|
||||||
logger.debug(`Runtime initialized for: ${pluginId}`);
|
logger.debug(`Runtime initialized for: ${pluginId}`);
|
||||||
@@ -661,22 +724,104 @@ export class PluginManager implements IService {
|
|||||||
/**
|
/**
|
||||||
* 动态卸载插件的运行时模块
|
* 动态卸载插件的运行时模块
|
||||||
* Dynamically deactivate plugin's runtime module
|
* 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 {
|
private deactivatePluginRuntime(pluginId: string): void {
|
||||||
const plugin = this.plugins.get(pluginId);
|
const plugin = this.plugins.get(pluginId);
|
||||||
if (!plugin) return;
|
if (!plugin) return;
|
||||||
|
|
||||||
const runtimeModule = plugin.plugin.runtimeModule;
|
const runtimeModule = plugin.plugin.runtimeModule;
|
||||||
if (!runtimeModule) return;
|
const resources = plugin.registeredResources;
|
||||||
|
|
||||||
// 调用销毁回调
|
// 1. 调用销毁回调
|
||||||
if (runtimeModule.onDestroy) {
|
// Step 1: Call destroy callback
|
||||||
|
if (runtimeModule?.onDestroy) {
|
||||||
|
try {
|
||||||
runtimeModule.onDestroy();
|
runtimeModule.onDestroy();
|
||||||
logger.debug(`Runtime destroyed for: ${pluginId}`);
|
logger.debug(`Runtime onDestroy called for: ${pluginId}`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Error in onDestroy for ${pluginId}:`, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注意:组件和服务无法动态注销,这是设计限制
|
if (!resources) {
|
||||||
// 系统的移除需要场景支持,暂时只调用 onDestroy
|
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)
|
* - scripts/editor/ -> Editor-only code (inspectors, gizmos, panels)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { IHotReloadOptions } from './HotReloadCoordinator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User code target environment.
|
* User code target environment.
|
||||||
* 用户代码目标环境。
|
* 用户代码目标环境。
|
||||||
@@ -280,8 +282,13 @@ export interface IUserCodeService {
|
|||||||
*
|
*
|
||||||
* @param projectPath - Project root path | 项目根路径
|
* @param projectPath - Project root path | 项目根路径
|
||||||
* @param onReload - Callback when code is reloaded | 代码重新加载时的回调
|
* @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.
|
* Stop watching for file changes.
|
||||||
@@ -296,20 +303,36 @@ export interface IUserCodeService {
|
|||||||
isWatching(): boolean;
|
isWatching(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { EditorConfig } from '../../Config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default scripts directory name.
|
* 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.
|
* 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.
|
* 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 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 {
|
import type {
|
||||||
IUserCodeService,
|
IUserCodeService,
|
||||||
UserScriptInfo,
|
UserScriptInfo,
|
||||||
@@ -15,7 +22,8 @@ import type {
|
|||||||
UserCodeCompileResult,
|
UserCodeCompileResult,
|
||||||
CompileError,
|
CompileError,
|
||||||
UserCodeModule,
|
UserCodeModule,
|
||||||
HotReloadEvent
|
HotReloadEvent,
|
||||||
|
IHotReloadOptions
|
||||||
} from './IUserCodeService';
|
} from './IUserCodeService';
|
||||||
import {
|
import {
|
||||||
UserCodeTarget,
|
UserCodeTarget,
|
||||||
@@ -23,6 +31,8 @@ import {
|
|||||||
EDITOR_SCRIPTS_DIR,
|
EDITOR_SCRIPTS_DIR,
|
||||||
USER_CODE_OUTPUT_DIR
|
USER_CODE_OUTPUT_DIR
|
||||||
} from './IUserCodeService';
|
} from './IUserCodeService';
|
||||||
|
import { HotReloadCoordinator, EHotReloadPhase } from './HotReloadCoordinator';
|
||||||
|
import { EditorConfig } from '../../Config';
|
||||||
import type { IFileSystem, FileEntry } from '../IFileSystem';
|
import type { IFileSystem, FileEntry } from '../IFileSystem';
|
||||||
import type { ComponentInspectorRegistry, IComponentInspector } from '../ComponentInspectorRegistry';
|
import type { ComponentInspectorRegistry, IComponentInspector } from '../ComponentInspectorRegistry';
|
||||||
import { GizmoRegistry } from '../../Gizmos/GizmoRegistry';
|
import { GizmoRegistry } from '../../Gizmos/GizmoRegistry';
|
||||||
@@ -64,8 +74,15 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
*/
|
*/
|
||||||
private _registeredGizmoTypes: any[] = [];
|
private _registeredGizmoTypes: any[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热更新协调器
|
||||||
|
* Hot reload coordinator
|
||||||
|
*/
|
||||||
|
private _hotReloadCoordinator: HotReloadCoordinator;
|
||||||
|
|
||||||
constructor(fileSystem: IFileSystem) {
|
constructor(fileSystem: IFileSystem) {
|
||||||
this._fileSystem = fileSystem;
|
this._fileSystem = fileSystem;
|
||||||
|
this._hotReloadCoordinator = new HotReloadCoordinator();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,8 +177,8 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
|
|
||||||
// Determine output file name | 确定输出文件名
|
// Determine output file name | 确定输出文件名
|
||||||
const outputFileName = options.target === UserCodeTarget.Runtime
|
const outputFileName = options.target === UserCodeTarget.Runtime
|
||||||
? 'user-runtime.js'
|
? EditorConfig.output.runtimeBundle
|
||||||
: 'user-editor.js';
|
: EditorConfig.output.editorBundle;
|
||||||
const outputPath = `${outputDir}${sep}${outputFileName}`;
|
const outputPath = `${outputDir}${sep}${outputFileName}`;
|
||||||
|
|
||||||
// Build entry point content | 构建入口点内容
|
// Build entry point content | 构建入口点内容
|
||||||
@@ -176,8 +193,8 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
|
|
||||||
// Determine global name for IIFE output | 确定 IIFE 输出的全局名称
|
// Determine global name for IIFE output | 确定 IIFE 输出的全局名称
|
||||||
const globalName = options.target === UserCodeTarget.Runtime
|
const globalName = options.target === UserCodeTarget.Runtime
|
||||||
? '__USER_RUNTIME_EXPORTS__'
|
? EditorConfig.globals.userRuntimeExports
|
||||||
: '__USER_EDITOR_EXPORTS__';
|
: EditorConfig.globals.userEditorExports;
|
||||||
|
|
||||||
// Build alias map for framework dependencies | 构建框架依赖的别名映射
|
// Build alias map for framework dependencies | 构建框架依赖的别名映射
|
||||||
const shimPath = `${outputDir}${sep}_shim_ecs_framework.js`.replace(/\\/g, '/');
|
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
|
// Access scene through Core.scene
|
||||||
// 通过 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;
|
const scene = Core?.scene;
|
||||||
if (!scene || !scene.entities) {
|
if (!scene || !scene.entities) {
|
||||||
logger.warn('No active scene for hot reload | 没有活动场景用于热更新');
|
logger.warn('No active scene for hot reload | 没有活动场景用于热更新');
|
||||||
@@ -526,9 +544,10 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
systemInstance.enabled = enabled;
|
systemInstance.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记为用户系统,便于后续识别和移除 | Mark as user system for later identification and removal
|
// 标记为用户系统,便于后续识别和移除
|
||||||
systemInstance.__isUserSystem__ = true;
|
// Mark as user system for later identification and removal
|
||||||
systemInstance.__userSystemName__ = name;
|
systemInstance[EditorConfig.typeMarkers.userSystem] = true;
|
||||||
|
systemInstance[EditorConfig.typeMarkers.userSystemName] = name;
|
||||||
|
|
||||||
// 添加到场景 | Add to scene
|
// 添加到场景 | Add to scene
|
||||||
scene.addSystem(systemInstance);
|
scene.addSystem(systemInstance);
|
||||||
@@ -565,9 +584,11 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
for (const system of this._registeredSystems) {
|
for (const system of this._registeredSystems) {
|
||||||
try {
|
try {
|
||||||
scene.removeSystem(system);
|
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) {
|
} 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 projectPath - Project root path | 项目根路径
|
||||||
* @param onReload - Callback when code is reloaded | 代码重新加载时的回调
|
* @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) {
|
if (this._watching) {
|
||||||
this._watchCallbacks.push(onReload);
|
this._watchCallbacks.push(onReload);
|
||||||
return;
|
return;
|
||||||
@@ -701,6 +727,16 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
|
|
||||||
this._currentProjectPath = projectPath;
|
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 {
|
try {
|
||||||
// Check if we're in Tauri environment | 检查是否在 Tauri 环境
|
// Check if we're in Tauri environment | 检查是否在 Tauri 环境
|
||||||
if (PlatformDetector.isTauriEnvironment()) {
|
if (PlatformDetector.isTauriEnvironment()) {
|
||||||
@@ -731,27 +767,44 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
// Get previous module | 获取之前的模块
|
// Get previous module | 获取之前的模块
|
||||||
const previousModule = this.getModule(target);
|
const previousModule = this.getModule(target);
|
||||||
|
|
||||||
|
// Use coordinator for synchronized hot reload
|
||||||
|
// 使用协调器进行同步热更新
|
||||||
|
try {
|
||||||
|
const hotReloadEvent = await this._hotReloadCoordinator.performHotReload(
|
||||||
|
async () => {
|
||||||
// Recompile the affected target | 重新编译受影响的目标
|
// Recompile the affected target | 重新编译受影响的目标
|
||||||
const compileResult = await this.compile({
|
const compileResult = await this.compile({
|
||||||
projectPath,
|
projectPath,
|
||||||
target
|
target
|
||||||
});
|
});
|
||||||
|
|
||||||
if (compileResult.success && compileResult.outputPath) {
|
if (!compileResult.success || !compileResult.outputPath) {
|
||||||
|
throw new Error(
|
||||||
|
`Hot reload compilation failed: ${compileResult.errors.map(e => e.message).join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Reload the module | 重新加载模块
|
// Reload the module | 重新加载模块
|
||||||
const newModule = await this.load(compileResult.outputPath, target);
|
const newModule = await this.load(compileResult.outputPath, target);
|
||||||
|
|
||||||
// Create hot reload event | 创建热更新事件
|
// Create hot reload event | 创建热更新事件
|
||||||
const hotReloadEvent: HotReloadEvent = {
|
const reloadEvent: HotReloadEvent = {
|
||||||
target,
|
target,
|
||||||
changedFiles: paths,
|
changedFiles: paths,
|
||||||
previousModule,
|
previousModule,
|
||||||
newModule
|
newModule
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return reloadEvent;
|
||||||
|
},
|
||||||
|
options
|
||||||
|
) as HotReloadEvent;
|
||||||
|
|
||||||
|
if (hotReloadEvent) {
|
||||||
this._notifyHotReload(hotReloadEvent);
|
this._notifyHotReload(hotReloadEvent);
|
||||||
} else {
|
}
|
||||||
logger.error('Hot reload compilation failed | 热更新编译失败', compileResult.errors);
|
} 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.
|
* Stop watching for file changes.
|
||||||
* 停止监视文件变更。
|
* 停止监视文件变更。
|
||||||
@@ -971,12 +1034,13 @@ export class UserCodeService implements IService, IUserCodeService {
|
|||||||
const shimPaths: string[] = [];
|
const shimPaths: string[] = [];
|
||||||
|
|
||||||
// Create shim for @esengine/ecs-framework | 为 @esengine/ecs-framework 创建 shim
|
// Create shim for @esengine/ecs-framework | 为 @esengine/ecs-framework 创建 shim
|
||||||
// This uses window.__ESENGINE__.ecsFramework set by PluginSDKRegistry
|
// This uses window[EditorConfig.globals.sdk].ecsFramework set by PluginSDKRegistry
|
||||||
// 这使用 PluginSDKRegistry 设置的 window.__ESENGINE__.ecsFramework
|
// 这使用 PluginSDKRegistry 设置的 window[EditorConfig.globals.sdk].ecsFramework
|
||||||
const ecsShimPath = `${outputDir}${sep}_shim_ecs_framework.js`;
|
const ecsShimPath = `${outputDir}${sep}_shim_ecs_framework.js`;
|
||||||
|
const sdkGlobalName = EditorConfig.globals.sdk;
|
||||||
const ecsShimContent = `// Shim for @esengine/ecs-framework
|
const ecsShimContent = `// Shim for @esengine/ecs-framework
|
||||||
// Maps to window.__ESENGINE__.ecsFramework set by PluginSDKRegistry
|
// Maps to window.${sdkGlobalName}.ecsFramework set by PluginSDKRegistry
|
||||||
module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window.__ESENGINE__.ecsFramework) || {};
|
module.exports = (typeof window !== 'undefined' && window.${sdkGlobalName} && window.${sdkGlobalName}.ecsFramework) || {};
|
||||||
`;
|
`;
|
||||||
await this._fileSystem.writeFile(ecsShimPath, ecsShimContent);
|
await this._fileSystem.writeFile(ecsShimPath, ecsShimContent);
|
||||||
shimPaths.push(ecsShimPath);
|
shimPaths.push(ecsShimPath);
|
||||||
@@ -1101,8 +1165,8 @@ module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window
|
|||||||
): Promise<Record<string, any>> {
|
): Promise<Record<string, any>> {
|
||||||
// Determine global name based on target | 根据目标确定全局名称
|
// Determine global name based on target | 根据目标确定全局名称
|
||||||
const globalName = target === UserCodeTarget.Runtime
|
const globalName = target === UserCodeTarget.Runtime
|
||||||
? '__USER_RUNTIME_EXPORTS__'
|
? EditorConfig.globals.userRuntimeExports
|
||||||
: '__USER_EDITOR_EXPORTS__';
|
: EditorConfig.globals.userEditorExports;
|
||||||
|
|
||||||
// Clear any previous exports | 清除之前的导出
|
// Clear any previous exports | 清除之前的导出
|
||||||
(window as any)[globalName] = undefined;
|
(window as any)[globalName] = undefined;
|
||||||
@@ -1150,47 +1214,45 @@ module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a class extends Component.
|
* Check if a class is decorated with @ECSComponent.
|
||||||
* 检查类是否继承自 Component。
|
* 检查类是否使用了 @ECSComponent 装饰器。
|
||||||
*
|
*
|
||||||
* Uses the actual Component class from the global framework to check inheritance.
|
* 用户组件必须使用 @ECSComponent 装饰器才能被识别。
|
||||||
* 使用全局框架中的实际 Component 类来检查继承关系。
|
* User components must use @ECSComponent decorator to be recognized.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @ECSComponent('MyComponent')
|
||||||
|
* class MyComponent extends Component {
|
||||||
|
* // ...
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
private _isComponentClass(cls: any): boolean {
|
private _isComponentClass(cls: any): boolean {
|
||||||
// Get Component class from global framework | 从全局框架获取 Component 类
|
// 检查是否有 @ECSComponent 装饰器(通过 Symbol 键)
|
||||||
const framework = (window as any).__ESENGINE__?.ecsFramework;
|
// Check if class has @ECSComponent decorator (via Symbol key)
|
||||||
|
return cls?.[COMPONENT_TYPE_NAME] !== undefined;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a class extends System.
|
* Check if a class is decorated with @ECSSystem.
|
||||||
* 检查类是否继承自 System。
|
* 检查类是否使用了 @ECSSystem 装饰器。
|
||||||
|
*
|
||||||
|
* 用户系统必须使用 @ECSSystem 装饰器才能被识别。
|
||||||
|
* User systems must use @ECSSystem decorator to be recognized.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @ECSSystem('MySystem')
|
||||||
|
* class MySystem extends EntitySystem {
|
||||||
|
* // ...
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
private _isSystemClass(cls: any): boolean {
|
private _isSystemClass(cls: any): boolean {
|
||||||
let proto = cls.prototype;
|
// 检查是否有 @ECSSystem 装饰器(通过 Symbol 键)
|
||||||
while (proto) {
|
// Check if class has @ECSSystem decorator (via Symbol key)
|
||||||
const name = proto.constructor?.name;
|
return cls?.[SYSTEM_TYPE_NAME] !== undefined;
|
||||||
if (name === 'System' || name === 'EntityProcessingSystem') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
proto = Object.getPrototypeOf(proto);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -102,14 +102,19 @@ export type {
|
|||||||
UserCodeCompileResult,
|
UserCodeCompileResult,
|
||||||
CompileError,
|
CompileError,
|
||||||
UserCodeModule,
|
UserCodeModule,
|
||||||
HotReloadEvent
|
HotReloadEvent,
|
||||||
|
IHotReloadStatus,
|
||||||
|
IHotReloadOptions
|
||||||
} from './IUserCodeService';
|
} from './IUserCodeService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
UserCodeTarget,
|
UserCodeTarget,
|
||||||
SCRIPTS_DIR,
|
SCRIPTS_DIR,
|
||||||
EDITOR_SCRIPTS_DIR,
|
EDITOR_SCRIPTS_DIR,
|
||||||
USER_CODE_OUTPUT_DIR
|
USER_CODE_OUTPUT_DIR,
|
||||||
|
EHotReloadPhase
|
||||||
} from './IUserCodeService';
|
} from './IUserCodeService';
|
||||||
|
|
||||||
export { UserCodeService } from './UserCodeService';
|
export { UserCodeService } from './UserCodeService';
|
||||||
|
|
||||||
|
export { HotReloadCoordinator } from './HotReloadCoordinator';
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
* Plugin-based editor framework for ECS Framework
|
* Plugin-based editor framework for ECS Framework
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 配置 | Configuration
|
||||||
|
export * from './Config';
|
||||||
|
|
||||||
// 新插件系统 | New plugin system
|
// 新插件系统 | New plugin system
|
||||||
export * from './Plugin';
|
export * from './Plugin';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user