refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
20
packages/engine/runtime-core/module.json
Normal file
20
packages/engine/runtime-core/module.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "runtime-core",
|
||||
"name": "@esengine/runtime-core",
|
||||
"globalKey": "runtimeCore",
|
||||
"displayName": "Runtime Core",
|
||||
"description": "Core runtime framework | 核心运行时框架",
|
||||
"version": "1.0.0",
|
||||
"category": "Core",
|
||||
"icon": "Play",
|
||||
"tags": ["runtime", "core", "framework"],
|
||||
"isCore": true,
|
||||
"defaultEnabled": true,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": false,
|
||||
"platforms": ["web", "desktop", "mobile"],
|
||||
"dependencies": ["core", "asset-system"],
|
||||
"exports": {},
|
||||
"requiresWasm": false,
|
||||
"outputPath": "dist/index.js"
|
||||
}
|
||||
45
packages/engine/runtime-core/package.json
Normal file
45
packages/engine/runtime-core/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@esengine/runtime-core",
|
||||
"version": "1.0.0",
|
||||
"description": "Runtime core - plugin management and system initialization",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/ecs-framework-math": "workspace:*",
|
||||
"@esengine/ecs-engine-bindgen": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/asset-system": "workspace:*",
|
||||
"@esengine/platform-common": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"runtime",
|
||||
"plugin"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT"
|
||||
}
|
||||
1109
packages/engine/runtime-core/src/GameRuntime.ts
Normal file
1109
packages/engine/runtime-core/src/GameRuntime.ts
Normal file
File diff suppressed because it is too large
Load Diff
163
packages/engine/runtime-core/src/IPlatformAdapter.ts
Normal file
163
packages/engine/runtime-core/src/IPlatformAdapter.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Platform Adapter Interface
|
||||
* 平台适配器接口
|
||||
*
|
||||
* 定义不同平台(编辑器、浏览器、原生)需要实现的适配器接口
|
||||
* Defines the adapter interface that different platforms need to implement
|
||||
*/
|
||||
|
||||
import type { IPlatformInputSubsystem } from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* 资源路径解析器
|
||||
* Asset path resolver
|
||||
*/
|
||||
export interface IPathResolver {
|
||||
/**
|
||||
* 解析资源路径为可加载的 URL
|
||||
* Resolve asset path to a loadable URL
|
||||
*/
|
||||
resolve(path: string): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台能力标识
|
||||
* Platform capability flags
|
||||
*/
|
||||
export interface PlatformCapabilities {
|
||||
/** 是否支持文件系统访问 / Supports file system access */
|
||||
fileSystem: boolean;
|
||||
/** 是否支持热重载 / Supports hot reload */
|
||||
hotReload: boolean;
|
||||
/** 是否支持 Gizmo 显示 / Supports gizmo display */
|
||||
gizmos: boolean;
|
||||
/** 是否支持网格显示 / Supports grid display */
|
||||
grid: boolean;
|
||||
/** 是否支持场景编辑 / Supports scene editing */
|
||||
sceneEditing: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台适配器配置
|
||||
* Platform adapter configuration
|
||||
*/
|
||||
export interface PlatformAdapterConfig {
|
||||
/** Canvas 元素 ID */
|
||||
canvasId: string;
|
||||
/** 初始宽度 */
|
||||
width?: number;
|
||||
/** 初始高度 */
|
||||
height?: number;
|
||||
/** 是否为编辑器模式 */
|
||||
isEditor?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台适配器接口
|
||||
* Platform adapter interface
|
||||
*
|
||||
* 不同平台通过实现此接口来提供平台特定的功能
|
||||
* Different platforms implement this interface to provide platform-specific functionality
|
||||
*/
|
||||
export interface IPlatformAdapter {
|
||||
/**
|
||||
* 平台名称
|
||||
* Platform name
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* 平台能力
|
||||
* Platform capabilities
|
||||
*/
|
||||
readonly capabilities: PlatformCapabilities;
|
||||
|
||||
/**
|
||||
* 路径解析器
|
||||
* Path resolver
|
||||
*/
|
||||
readonly pathResolver: IPathResolver;
|
||||
|
||||
/**
|
||||
* 初始化平台
|
||||
* Initialize platform
|
||||
*/
|
||||
initialize(config: PlatformAdapterConfig): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取 WASM 模块
|
||||
* Get WASM module
|
||||
*
|
||||
* 不同平台可能以不同方式加载 WASM
|
||||
* Different platforms may load WASM in different ways
|
||||
*/
|
||||
getWasmModule(): Promise<any>;
|
||||
|
||||
/**
|
||||
* 获取 Canvas 元素
|
||||
* Get canvas element
|
||||
*/
|
||||
getCanvas(): HTMLCanvasElement | null;
|
||||
|
||||
/**
|
||||
* 调整视口大小
|
||||
* Resize viewport
|
||||
*/
|
||||
resize(width: number, height: number): void;
|
||||
|
||||
/**
|
||||
* 获取当前视口尺寸
|
||||
* Get current viewport size
|
||||
*/
|
||||
getViewportSize(): { width: number; height: number };
|
||||
|
||||
/**
|
||||
* 是否为编辑器模式
|
||||
* Whether in editor mode
|
||||
*/
|
||||
isEditorMode(): boolean;
|
||||
|
||||
/**
|
||||
* 设置是否显示网格(仅编辑器模式有效)
|
||||
* Set whether to show grid (only effective in editor mode)
|
||||
*/
|
||||
setShowGrid?(show: boolean): void;
|
||||
|
||||
/**
|
||||
* 设置是否显示 Gizmos(仅编辑器模式有效)
|
||||
* Set whether to show gizmos (only effective in editor mode)
|
||||
*/
|
||||
setShowGizmos?(show: boolean): void;
|
||||
|
||||
/**
|
||||
* 获取输入子系统
|
||||
* Get input subsystem
|
||||
*
|
||||
* 返回平台特定的输入子系统实现
|
||||
* Returns platform-specific input subsystem implementation
|
||||
*/
|
||||
getInputSubsystem?(): IPlatformInputSubsystem | null;
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
* Dispose resources
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认路径解析器(直接返回路径)
|
||||
* Default path resolver (returns path as-is)
|
||||
*/
|
||||
export class DefaultPathResolver implements IPathResolver {
|
||||
resolve(path: string): string {
|
||||
// 如果已经是 URL,直接返回
|
||||
if (path.startsWith('http://') ||
|
||||
path.startsWith('https://') ||
|
||||
path.startsWith('data:') ||
|
||||
path.startsWith('blob:')) {
|
||||
return path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
247
packages/engine/runtime-core/src/ImportMapGenerator.ts
Normal file
247
packages/engine/runtime-core/src/ImportMapGenerator.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* @zh ImportMap 生成器
|
||||
* @en ImportMap Generator
|
||||
*
|
||||
* @zh 提供统一的 ImportMap 生成逻辑,供编辑器预览和构建共用
|
||||
* @en Provides unified ImportMap generation logic for editor preview and build
|
||||
*/
|
||||
|
||||
import type { ModuleManifest } from './PluginManager';
|
||||
import {
|
||||
extractShortId,
|
||||
getPackageName as getPackageNameFromId,
|
||||
topologicalSort,
|
||||
type IDependable
|
||||
} from './utils/DependencyUtils';
|
||||
|
||||
/**
|
||||
* @zh ImportMap 生成模式
|
||||
* @en ImportMap generation mode
|
||||
*/
|
||||
export type ImportMapMode =
|
||||
| 'development' // 开发模式:每个模块单独文件
|
||||
| 'production' // 生产模式:核心模块打包,插件分离
|
||||
| 'single-bundle'; // 单文件模式:所有模块打包到一个文件
|
||||
|
||||
/**
|
||||
* @zh ImportMap 生成配置
|
||||
* @en ImportMap generation configuration
|
||||
*/
|
||||
export interface ImportMapConfig {
|
||||
/**
|
||||
* @zh 生成模式
|
||||
* @en Generation mode
|
||||
*/
|
||||
mode: ImportMapMode;
|
||||
|
||||
/**
|
||||
* @zh 基础路径(用于构造相对 URL)
|
||||
* @en Base path (for constructing relative URLs)
|
||||
*/
|
||||
basePath?: string;
|
||||
|
||||
/**
|
||||
* @zh 核心模块列表
|
||||
* @en List of core modules
|
||||
*/
|
||||
coreModules: ModuleManifest[];
|
||||
|
||||
/**
|
||||
* @zh 插件模块列表
|
||||
* @en List of plugin modules
|
||||
*/
|
||||
pluginModules?: ModuleManifest[];
|
||||
|
||||
/**
|
||||
* @zh 自定义路径生成器(可选)
|
||||
* @en Custom path generator (optional)
|
||||
*/
|
||||
pathGenerator?: (module: ModuleManifest, isCore: boolean) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh ImportMap 条目
|
||||
* @en ImportMap entry
|
||||
*/
|
||||
export interface ImportMapEntry {
|
||||
/**
|
||||
* @zh 包名(如 @esengine/ecs-framework)
|
||||
* @en Package name (e.g., @esengine/ecs-framework)
|
||||
*/
|
||||
packageName: string;
|
||||
|
||||
/**
|
||||
* @zh 模块路径(相对 URL)
|
||||
* @en Module path (relative URL)
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* @zh 模块 ID(如 core)
|
||||
* @en Module ID (e.g., core)
|
||||
*/
|
||||
moduleId: string;
|
||||
|
||||
/**
|
||||
* @zh 是否为核心模块
|
||||
* @en Whether it's a core module
|
||||
*/
|
||||
isCore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成 ImportMap
|
||||
* @en Generate ImportMap
|
||||
*/
|
||||
export function generateImportMap(config: ImportMapConfig): Record<string, string> {
|
||||
const imports: Record<string, string> = {};
|
||||
const basePath = config.basePath || '.';
|
||||
|
||||
// 根据模式选择路径生成策略
|
||||
const getModulePath = config.pathGenerator || ((module: ModuleManifest, isCore: boolean) => {
|
||||
switch (config.mode) {
|
||||
case 'development':
|
||||
// 开发模式:每个模块单独文件
|
||||
return `${basePath}/libs/${module.id}/${module.id}.js`;
|
||||
|
||||
case 'production':
|
||||
// 生产模式:核心模块打包,插件分离
|
||||
if (isCore) {
|
||||
return `${basePath}/libs/esengine.core.js`;
|
||||
}
|
||||
return `${basePath}/libs/plugins/${module.id}.js`;
|
||||
|
||||
case 'single-bundle':
|
||||
// 单文件模式:所有模块打包
|
||||
return `${basePath}/libs/esengine.bundle.js`;
|
||||
}
|
||||
});
|
||||
|
||||
// 处理核心模块
|
||||
for (const module of config.coreModules) {
|
||||
if (module.name) {
|
||||
imports[module.name] = getModulePath(module, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理插件模块
|
||||
if (config.pluginModules) {
|
||||
for (const module of config.pluginModules) {
|
||||
if (module.name) {
|
||||
imports[module.name] = getModulePath(module, false);
|
||||
}
|
||||
|
||||
// 处理外部依赖
|
||||
if (module.externalDependencies) {
|
||||
for (const dep of module.externalDependencies) {
|
||||
if (!imports[dep]) {
|
||||
const depId = extractModuleId(dep);
|
||||
imports[dep] = `${basePath}/libs/${config.mode === 'development' ? `${depId}/${depId}` : `plugins/${depId}`}.js`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成 ImportMap 条目列表
|
||||
* @en Generate ImportMap entry list
|
||||
*/
|
||||
export function generateImportMapEntries(config: ImportMapConfig): ImportMapEntry[] {
|
||||
const entries: ImportMapEntry[] = [];
|
||||
const importMap = generateImportMap(config);
|
||||
|
||||
for (const [packageName, path] of Object.entries(importMap)) {
|
||||
const moduleId = extractModuleId(packageName);
|
||||
const isCore = config.coreModules.some(m => m.name === packageName);
|
||||
|
||||
entries.push({
|
||||
packageName,
|
||||
path,
|
||||
moduleId,
|
||||
isCore
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 生成 ImportMap HTML 脚本标签
|
||||
* @en Generate ImportMap HTML script tag
|
||||
*
|
||||
* @param imports - @zh ImportMap 对象 @en ImportMap object
|
||||
* @param indent - @zh 缩进空格数 @en Number of indent spaces
|
||||
*/
|
||||
export function generateImportMapScript(imports: Record<string, string>, indent = 4): string {
|
||||
const indentStr = ' '.repeat(indent);
|
||||
const json = JSON.stringify({ imports }, null, 2)
|
||||
.split('\n')
|
||||
.map((line, i) => i === 0 ? line : indentStr + line)
|
||||
.join('\n');
|
||||
|
||||
return `<script type="importmap">
|
||||
${indentStr}${json}
|
||||
${indentStr}</script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从包名提取模块 ID
|
||||
* @en Extract module ID from package name
|
||||
*
|
||||
* @zh 重新导出自 DependencyUtils(保持向后兼容)
|
||||
* @en Re-exported from DependencyUtils (for backward compatibility)
|
||||
*
|
||||
* @example
|
||||
* extractModuleId('@esengine/ecs-framework') // 'core'
|
||||
* extractModuleId('@esengine/sprite') // 'sprite'
|
||||
*/
|
||||
export const extractModuleId = extractShortId;
|
||||
|
||||
/**
|
||||
* @zh 从模块 ID 获取包名
|
||||
* @en Get package name from module ID
|
||||
*
|
||||
* @zh 重新导出自 DependencyUtils(保持向后兼容)
|
||||
* @en Re-exported from DependencyUtils (for backward compatibility)
|
||||
*
|
||||
* @example
|
||||
* getPackageName('core') // '@esengine/ecs-framework'
|
||||
* getPackageName('sprite') // '@esengine/sprite'
|
||||
*/
|
||||
export const getPackageName = getPackageNameFromId;
|
||||
|
||||
/**
|
||||
* @zh 收集模块的外部依赖
|
||||
* @en Collect external dependencies of modules
|
||||
*/
|
||||
export function collectExternalDependencies(modules: ModuleManifest[]): Set<string> {
|
||||
const deps = new Set<string>();
|
||||
|
||||
for (const module of modules) {
|
||||
if (module.externalDependencies) {
|
||||
for (const dep of module.externalDependencies) {
|
||||
deps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 按依赖排序模块(拓扑排序)
|
||||
* @en Sort modules by dependencies (topological sort)
|
||||
*
|
||||
* @zh 使用统一的 DependencyUtils.topologicalSort
|
||||
* @en Uses unified DependencyUtils.topologicalSort
|
||||
*/
|
||||
export function sortModulesByDependencies<T extends IDependable>(
|
||||
modules: T[]
|
||||
): T[] {
|
||||
const result = topologicalSort(modules, { algorithm: 'dfs' });
|
||||
return result.sorted;
|
||||
}
|
||||
415
packages/engine/runtime-core/src/PluginLoader.ts
Normal file
415
packages/engine/runtime-core/src/PluginLoader.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
/**
|
||||
* @zh 插件加载器
|
||||
* @en Plugin Loader
|
||||
*
|
||||
* @zh 提供统一的插件加载机制,支持:
|
||||
* - 动态 ESM 导入
|
||||
* - 依赖拓扑排序
|
||||
* - 加载状态追踪
|
||||
* - 错误隔离
|
||||
*
|
||||
* @en Provides unified plugin loading with:
|
||||
* - Dynamic ESM imports
|
||||
* - Dependency topological sorting
|
||||
* - Load state tracking
|
||||
* - Error isolation
|
||||
*/
|
||||
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import type { IRuntimePlugin, ModuleManifest } from './PluginManager';
|
||||
import { runtimePluginManager } from './PluginManager';
|
||||
import {
|
||||
topologicalSort,
|
||||
validateDependencies as validateDeps,
|
||||
resolveDependencyId,
|
||||
type IDependable
|
||||
} from './utils/DependencyUtils';
|
||||
|
||||
const logger = createLogger('PluginLoader');
|
||||
|
||||
// ============================================================================
|
||||
// 类型定义 | Types
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 插件加载状态
|
||||
* @en Plugin load state
|
||||
*/
|
||||
export type PluginLoadState =
|
||||
| 'pending' // 等待加载
|
||||
| 'loading' // 加载中
|
||||
| 'loaded' // 已加载
|
||||
| 'failed' // 加载失败
|
||||
| 'missing'; // 依赖缺失
|
||||
|
||||
/**
|
||||
* @zh 插件源类型
|
||||
* @en Plugin source type
|
||||
*/
|
||||
export type PluginSourceType = 'npm' | 'local' | 'static';
|
||||
|
||||
/**
|
||||
* @zh 插件包信息
|
||||
* @en Plugin package info
|
||||
*/
|
||||
export interface PluginPackageInfo {
|
||||
plugin: boolean;
|
||||
pluginExport: string;
|
||||
category?: string;
|
||||
isEnginePlugin?: boolean;
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 插件配置
|
||||
* @en Plugin configuration
|
||||
*/
|
||||
export interface PluginConfig {
|
||||
enabled: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 项目插件配置
|
||||
* @en Project plugin configuration
|
||||
*/
|
||||
export interface ProjectPluginConfig {
|
||||
plugins: Record<string, PluginConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 插件加载配置
|
||||
* @en Plugin load configuration
|
||||
*/
|
||||
export interface PluginLoadConfig {
|
||||
packageId: string;
|
||||
enabled: boolean;
|
||||
sourceType: PluginSourceType;
|
||||
exportName?: string;
|
||||
dependencies?: string[];
|
||||
options?: Record<string, unknown>;
|
||||
localPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 插件加载信息
|
||||
* @en Plugin load info
|
||||
*/
|
||||
export interface PluginLoadInfo {
|
||||
packageId: string;
|
||||
state: PluginLoadState;
|
||||
plugin?: IRuntimePlugin;
|
||||
error?: string;
|
||||
missingDeps?: string[];
|
||||
loadTime?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加载器配置
|
||||
* @en Loader configuration
|
||||
*/
|
||||
export interface PluginLoaderConfig {
|
||||
plugins: PluginLoadConfig[];
|
||||
timeout?: number;
|
||||
continueOnFailure?: boolean;
|
||||
localLoader?: (path: string) => Promise<string>;
|
||||
localExecutor?: (code: string, id: string) => Promise<IRuntimePlugin | null>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 插件加载器 | Plugin Loader
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 插件加载器
|
||||
* @en Plugin Loader
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const loader = new PluginLoader({
|
||||
* plugins: [
|
||||
* { packageId: '@esengine/sprite', enabled: true, sourceType: 'npm' }
|
||||
* ]
|
||||
* });
|
||||
* await loader.loadAll();
|
||||
* ```
|
||||
*/
|
||||
export class PluginLoader {
|
||||
private _config: Required<PluginLoaderConfig>;
|
||||
private _loaded = new Map<string, PluginLoadInfo>();
|
||||
private _loading = false;
|
||||
|
||||
constructor(config: PluginLoaderConfig) {
|
||||
this._config = {
|
||||
plugins: config.plugins,
|
||||
timeout: config.timeout ?? 30000,
|
||||
continueOnFailure: config.continueOnFailure ?? true,
|
||||
localLoader: config.localLoader ?? (async () => ''),
|
||||
localExecutor: config.localExecutor ?? (async () => null)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加载所有启用的插件
|
||||
* @en Load all enabled plugins
|
||||
*/
|
||||
async loadAll(): Promise<Map<string, PluginLoadInfo>> {
|
||||
if (this._loading) {
|
||||
throw new Error('Loading already in progress');
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const enabled = this._config.plugins.filter(p => p.enabled);
|
||||
|
||||
// 验证依赖
|
||||
const missing = this._validateDependencies(enabled);
|
||||
for (const [id, deps] of missing) {
|
||||
this._loaded.set(id, {
|
||||
packageId: id,
|
||||
state: 'missing',
|
||||
missingDeps: deps,
|
||||
error: `Missing: ${deps.join(', ')}`
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤有效插件并排序
|
||||
const valid = enabled.filter(p => !missing.has(p.packageId));
|
||||
const sorted = this._sortByDependencies(valid);
|
||||
|
||||
// 串行加载
|
||||
for (const plugin of sorted) {
|
||||
await this._loadOne(plugin);
|
||||
}
|
||||
|
||||
const time = Date.now() - start;
|
||||
const loadedCount = this.getLoaded().length;
|
||||
logger.info(`Loaded ${loadedCount}/${enabled.length} plugins in ${time}ms`);
|
||||
|
||||
return this._loaded;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取已加载插件
|
||||
* @en Get loaded plugins
|
||||
*/
|
||||
getLoaded(): PluginLoadInfo[] {
|
||||
return [...this._loaded.values()].filter(p => p.state === 'loaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取失败的插件
|
||||
* @en Get failed plugins
|
||||
*/
|
||||
getFailed(): PluginLoadInfo[] {
|
||||
return [...this._loaded.values()].filter(
|
||||
p => p.state === 'failed' || p.state === 'missing'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取插件信息
|
||||
* @en Get plugin info
|
||||
*/
|
||||
get(packageId: string): PluginLoadInfo | undefined {
|
||||
return this._loaded.get(packageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置
|
||||
* @en Reset
|
||||
*/
|
||||
reset(): void {
|
||||
this._loaded.clear();
|
||||
}
|
||||
|
||||
// ========== 私有方法 ==========
|
||||
|
||||
private async _loadOne(config: PluginLoadConfig): Promise<void> {
|
||||
const info: PluginLoadInfo = {
|
||||
packageId: config.packageId,
|
||||
state: 'loading'
|
||||
};
|
||||
this._loaded.set(config.packageId, info);
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
let plugin: IRuntimePlugin | null = null;
|
||||
|
||||
switch (config.sourceType) {
|
||||
case 'npm':
|
||||
plugin = await this._loadNpm(config);
|
||||
break;
|
||||
case 'local':
|
||||
plugin = await this._loadLocal(config);
|
||||
break;
|
||||
case 'static':
|
||||
logger.warn(`Static plugin ${config.packageId} should be pre-registered`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (plugin) {
|
||||
info.plugin = plugin;
|
||||
info.state = 'loaded';
|
||||
info.loadTime = Date.now() - start;
|
||||
runtimePluginManager.register(plugin);
|
||||
logger.debug(`Loaded: ${config.packageId} (${info.loadTime}ms)`);
|
||||
} else {
|
||||
throw new Error('Plugin export not found');
|
||||
}
|
||||
} catch (error) {
|
||||
info.state = 'failed';
|
||||
info.error = error instanceof Error ? error.message : String(error);
|
||||
info.loadTime = Date.now() - start;
|
||||
logger.error(`Failed: ${config.packageId} - ${info.error}`);
|
||||
|
||||
if (!this._config.continueOnFailure) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadNpm(config: PluginLoadConfig): Promise<IRuntimePlugin | null> {
|
||||
const module = await import(/* @vite-ignore */ config.packageId);
|
||||
const exportName = config.exportName || 'default';
|
||||
const plugin = module[exportName] as IRuntimePlugin;
|
||||
return plugin?.manifest ? plugin : null;
|
||||
}
|
||||
|
||||
private async _loadLocal(config: PluginLoadConfig): Promise<IRuntimePlugin | null> {
|
||||
if (!config.localPath) {
|
||||
throw new Error('Local path not specified');
|
||||
}
|
||||
const code = await this._config.localLoader(config.localPath);
|
||||
return this._config.localExecutor(code, config.packageId);
|
||||
}
|
||||
|
||||
private _sortByDependencies(plugins: PluginLoadConfig[]): PluginLoadConfig[] {
|
||||
const items: IDependable[] = plugins.map(p => ({
|
||||
id: p.packageId,
|
||||
dependencies: p.dependencies
|
||||
}));
|
||||
const map = new Map(plugins.map(p => [p.packageId, p]));
|
||||
|
||||
const result = topologicalSort(items, { resolveId: resolveDependencyId });
|
||||
if (result.hasCycles) {
|
||||
throw new Error(`Circular dependency: ${result.cycleIds?.join(', ')}`);
|
||||
}
|
||||
|
||||
return result.sorted.map(item => map.get(item.id)!);
|
||||
}
|
||||
|
||||
private _validateDependencies(plugins: PluginLoadConfig[]): Map<string, string[]> {
|
||||
const enabledIds = new Set(plugins.map(p => p.packageId));
|
||||
const missing = new Map<string, string[]>();
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const deps = plugin.dependencies || [];
|
||||
const missingDeps = deps
|
||||
.map(d => resolveDependencyId(d))
|
||||
.filter(d => !enabledIds.has(d));
|
||||
|
||||
if (missingDeps.length > 0) {
|
||||
missing.set(plugin.packageId, missingDeps);
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 便捷函数 | Convenience Functions
|
||||
// ============================================================================
|
||||
|
||||
/** @zh 已加载插件缓存 @en Loaded plugins cache */
|
||||
const loadedCache = new Map<string, IRuntimePlugin>();
|
||||
|
||||
/**
|
||||
* @zh 加载单个插件
|
||||
* @en Load single plugin
|
||||
*/
|
||||
export async function loadPlugin(
|
||||
packageId: string,
|
||||
info: PluginPackageInfo
|
||||
): Promise<IRuntimePlugin | null> {
|
||||
if (loadedCache.has(packageId)) {
|
||||
return loadedCache.get(packageId)!;
|
||||
}
|
||||
|
||||
try {
|
||||
const module = await import(/* @vite-ignore */ packageId);
|
||||
const plugin = module[info.pluginExport || 'default'] as IRuntimePlugin;
|
||||
|
||||
if (!plugin?.manifest) {
|
||||
logger.warn(`Invalid plugin: ${packageId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
loadedCache.set(packageId, plugin);
|
||||
return plugin;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load ${packageId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加载启用的插件(简化 API)
|
||||
* @en Load enabled plugins (simplified API)
|
||||
*/
|
||||
export async function loadEnabledPlugins(
|
||||
config: ProjectPluginConfig,
|
||||
packageInfoMap: Record<string, PluginPackageInfo>
|
||||
): Promise<void> {
|
||||
const plugins: PluginLoadConfig[] = [];
|
||||
|
||||
for (const [id, cfg] of Object.entries(config.plugins)) {
|
||||
if (!cfg.enabled) continue;
|
||||
const info = packageInfoMap[id];
|
||||
if (!info) {
|
||||
logger.warn(`No package info for ${id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
plugins.push({
|
||||
packageId: id,
|
||||
enabled: true,
|
||||
sourceType: 'npm',
|
||||
exportName: info.pluginExport,
|
||||
dependencies: info.dependencies
|
||||
});
|
||||
}
|
||||
|
||||
const loader = new PluginLoader({ plugins });
|
||||
await loader.loadAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注册静态插件
|
||||
* @en Register static plugin
|
||||
*/
|
||||
export function registerStaticPlugin(plugin: IRuntimePlugin): void {
|
||||
runtimePluginManager.register(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取已加载插件
|
||||
* @en Get loaded plugins
|
||||
*/
|
||||
export function getLoadedPlugins(): IRuntimePlugin[] {
|
||||
return [...loadedCache.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置加载器
|
||||
* @en Reset loader
|
||||
*/
|
||||
export function resetPluginLoader(): void {
|
||||
loadedCache.clear();
|
||||
}
|
||||
601
packages/engine/runtime-core/src/PluginManager.ts
Normal file
601
packages/engine/runtime-core/src/PluginManager.ts
Normal file
@@ -0,0 +1,601 @@
|
||||
/**
|
||||
* @zh 运行时插件管理器
|
||||
* @en Runtime Plugin Manager
|
||||
*
|
||||
* @zh 提供插件生命周期管理的核心实现。
|
||||
* @en Provides core implementation for plugin lifecycle management.
|
||||
*
|
||||
* @zh 设计原则 | Design principles:
|
||||
* @en
|
||||
* 1. 最小依赖 - 只依赖 ecs-framework 和 engine-core
|
||||
* 2. 状态跟踪 - 详细的插件状态用于调试和 UI
|
||||
* 3. 依赖验证 - 确保加载顺序正确
|
||||
* 4. 错误隔离 - 单个插件失败不影响其他
|
||||
*/
|
||||
|
||||
import { GlobalComponentRegistry, ServiceContainer, createLogger } from '@esengine/ecs-framework';
|
||||
import type { IScene } from '@esengine/ecs-framework';
|
||||
import type { IRuntimePlugin, IRuntimeModule, SystemContext, ModuleManifest } from '@esengine/engine-core';
|
||||
import {
|
||||
topologicalSort,
|
||||
resolveDependencyId,
|
||||
getReverseDependencies,
|
||||
type IDependable
|
||||
} from './utils/DependencyUtils';
|
||||
import type { PluginState } from './PluginState';
|
||||
|
||||
export type { IRuntimePlugin, IRuntimeModule, SystemContext, ModuleManifest };
|
||||
|
||||
const logger = createLogger('PluginManager');
|
||||
|
||||
// ============================================================================
|
||||
// 类型定义 | Type Definitions
|
||||
// ============================================================================
|
||||
|
||||
// PluginState 从 ./PluginState 重新导出
|
||||
// PluginState is re-exported from ./PluginState
|
||||
|
||||
/**
|
||||
* @zh 已注册的插件信息
|
||||
* @en Registered plugin info
|
||||
*/
|
||||
export interface RegisteredPluginInfo {
|
||||
/**
|
||||
* @zh 插件实例
|
||||
* @en Plugin instance
|
||||
*/
|
||||
plugin: IRuntimePlugin;
|
||||
|
||||
/**
|
||||
* @zh 插件状态
|
||||
* @en Plugin state
|
||||
*/
|
||||
state: PluginState;
|
||||
|
||||
/**
|
||||
* @zh 是否启用
|
||||
* @en Whether enabled
|
||||
*/
|
||||
enabled: boolean;
|
||||
|
||||
/**
|
||||
* @zh 错误信息
|
||||
* @en Error message
|
||||
*/
|
||||
error?: Error;
|
||||
|
||||
/**
|
||||
* @zh 注册时间
|
||||
* @en Registration time
|
||||
*/
|
||||
registeredAt: number;
|
||||
|
||||
/**
|
||||
* @zh 激活时间
|
||||
* @en Activation time
|
||||
*/
|
||||
activatedAt?: number;
|
||||
|
||||
/**
|
||||
* @zh 创建的系统实例(用于清理)
|
||||
* @en Created system instances (for cleanup)
|
||||
*/
|
||||
systemInstances?: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 插件配置
|
||||
* @en Plugin configuration
|
||||
*/
|
||||
export interface RuntimePluginConfig {
|
||||
/**
|
||||
* @zh 启用的插件 ID 列表
|
||||
* @en Enabled plugin ID list
|
||||
*/
|
||||
enabledPlugins: string[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RuntimePluginManager
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 运行时插件管理器
|
||||
* @en Runtime Plugin Manager
|
||||
*
|
||||
* @zh 管理运行时插件的注册、初始化和生命周期。
|
||||
* @en Manages registration, initialization, and lifecycle of runtime plugins.
|
||||
*/
|
||||
export class RuntimePluginManager {
|
||||
private _plugins = new Map<string, RegisteredPluginInfo>();
|
||||
private _initialized = false;
|
||||
private _currentScene: IScene | null = null;
|
||||
private _currentContext: SystemContext | null = null;
|
||||
|
||||
// ============================================================================
|
||||
// 注册 | Registration
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 注册插件
|
||||
* @en Register plugin
|
||||
*
|
||||
* @param plugin - @zh 插件实例 @en Plugin instance
|
||||
*/
|
||||
register(plugin: IRuntimePlugin): void {
|
||||
if (!plugin?.manifest?.id) {
|
||||
logger.error('Cannot register plugin: invalid manifest');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = plugin.manifest.id;
|
||||
|
||||
if (this._plugins.has(id)) {
|
||||
logger.warn(`Plugin ${id} is already registered, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
const enabled = plugin.manifest.isCore === true ||
|
||||
plugin.manifest.isEngineModule === true ||
|
||||
plugin.manifest.defaultEnabled !== false;
|
||||
|
||||
this._plugins.set(id, {
|
||||
plugin,
|
||||
state: 'loading', // 已加载但未初始化
|
||||
enabled,
|
||||
registeredAt: Date.now()
|
||||
});
|
||||
|
||||
logger.debug(`Plugin registered: ${id}`, {
|
||||
enabled,
|
||||
isCore: plugin.manifest.isCore,
|
||||
isEngineModule: plugin.manifest.isEngineModule
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 批量注册插件
|
||||
* @en Register multiple plugins
|
||||
*
|
||||
* @param plugins - @zh 插件列表 @en Plugin list
|
||||
*/
|
||||
registerMany(plugins: IRuntimePlugin[]): void {
|
||||
for (const plugin of plugins) {
|
||||
this.register(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 启用/禁用 | Enable/Disable
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 启用插件
|
||||
* @en Enable plugin
|
||||
*
|
||||
* @param pluginId - @zh 插件 ID @en Plugin ID
|
||||
* @returns @zh 是否成功 @en Whether successful
|
||||
*/
|
||||
enable(pluginId: string): boolean {
|
||||
const info = this._plugins.get(pluginId);
|
||||
if (!info) {
|
||||
logger.error(`Plugin ${pluginId} not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.plugin.manifest.isCore) {
|
||||
logger.warn(`Core plugin ${pluginId} is always enabled`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查依赖 | Check dependencies
|
||||
const deps = info.plugin.manifest.dependencies || [];
|
||||
for (const dep of deps) {
|
||||
const depId = resolveDependencyId(dep);
|
||||
const depInfo = this._plugins.get(depId);
|
||||
if (!depInfo?.enabled) {
|
||||
logger.error(`Cannot enable ${pluginId}: dependency ${dep} is not enabled`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
info.enabled = true;
|
||||
logger.info(`Plugin enabled: ${pluginId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 禁用插件
|
||||
* @en Disable plugin
|
||||
*
|
||||
* @param pluginId - @zh 插件 ID @en Plugin ID
|
||||
* @returns @zh 是否成功 @en Whether successful
|
||||
*/
|
||||
disable(pluginId: string): boolean {
|
||||
const info = this._plugins.get(pluginId);
|
||||
if (!info) {
|
||||
logger.error(`Plugin ${pluginId} not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.plugin.manifest.isCore) {
|
||||
logger.warn(`Core plugin ${pluginId} cannot be disabled`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有其他插件依赖此插件(使用统一工具)
|
||||
// Check if other plugins depend on this (using unified util)
|
||||
const reverseDeps = this._getReverseDependencies(pluginId);
|
||||
const enabledReverseDeps = Array.from(reverseDeps).filter(
|
||||
id => this._plugins.get(id)?.enabled
|
||||
);
|
||||
if (enabledReverseDeps.length > 0) {
|
||||
logger.error(`Cannot disable ${pluginId}: plugins ${enabledReverseDeps.join(', ')} depend on it`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 清理系统实例 | Cleanup system instances
|
||||
if (info.systemInstances && this._currentScene) {
|
||||
for (const system of info.systemInstances) {
|
||||
try {
|
||||
this._currentScene.removeSystem(system);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to remove system from ${pluginId}:`, e);
|
||||
}
|
||||
}
|
||||
info.systemInstances = [];
|
||||
}
|
||||
|
||||
info.enabled = false;
|
||||
info.state = 'disabled';
|
||||
logger.info(`Plugin disabled: ${pluginId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查插件是否启用
|
||||
* @en Check if plugin is enabled
|
||||
*/
|
||||
isEnabled(pluginId: string): boolean {
|
||||
return this._plugins.get(pluginId)?.enabled ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加载配置
|
||||
* @en Load configuration
|
||||
*
|
||||
* @param config - @zh 插件配置 @en Plugin configuration
|
||||
*/
|
||||
loadConfig(config: RuntimePluginConfig): void {
|
||||
const { enabledPlugins } = config;
|
||||
|
||||
for (const [id, info] of this._plugins) {
|
||||
if (info.plugin.manifest.isCore || info.plugin.manifest.isEngineModule) {
|
||||
info.enabled = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const shouldEnable = enabledPlugins.includes(id) ||
|
||||
info.plugin.manifest.defaultEnabled === true;
|
||||
info.enabled = shouldEnable;
|
||||
}
|
||||
|
||||
logger.info('Plugin configuration loaded', {
|
||||
enabled: Array.from(this._plugins.values()).filter(p => p.enabled).length,
|
||||
total: this._plugins.size
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 初始化 | Initialization
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 初始化所有启用的插件
|
||||
* @en Initialize all enabled plugins
|
||||
*
|
||||
* @param services - @zh 服务容器 @en Service container
|
||||
*/
|
||||
async initializeRuntime(services: ServiceContainer): Promise<void> {
|
||||
if (this._initialized) {
|
||||
logger.warn('Runtime already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const sortedPlugins = this._topologicalSort();
|
||||
|
||||
// Phase 1: 注册组件 | Register components
|
||||
for (const pluginId of sortedPlugins) {
|
||||
const info = this._plugins.get(pluginId);
|
||||
if (!info?.enabled) continue;
|
||||
|
||||
const mod = info.plugin.runtimeModule;
|
||||
if (mod?.registerComponents) {
|
||||
try {
|
||||
info.state = 'initializing';
|
||||
mod.registerComponents(GlobalComponentRegistry);
|
||||
logger.debug(`Components registered for: ${pluginId}`);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to register components for ${pluginId}:`, e);
|
||||
info.state = 'error';
|
||||
info.error = e as Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: 注册服务 | Register services
|
||||
for (const pluginId of sortedPlugins) {
|
||||
const info = this._plugins.get(pluginId);
|
||||
if (!info?.enabled || info.state === 'error') continue;
|
||||
|
||||
const mod = info.plugin.runtimeModule;
|
||||
if (mod?.registerServices) {
|
||||
try {
|
||||
mod.registerServices(services);
|
||||
logger.debug(`Services registered for: ${pluginId}`);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to register services for ${pluginId}:`, e);
|
||||
info.state = 'error';
|
||||
info.error = e as Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: 初始化回调 | Initialize callbacks
|
||||
for (const pluginId of sortedPlugins) {
|
||||
const info = this._plugins.get(pluginId);
|
||||
if (!info?.enabled || info.state === 'error') continue;
|
||||
|
||||
const mod = info.plugin.runtimeModule;
|
||||
if (mod?.onInitialize) {
|
||||
try {
|
||||
await mod.onInitialize();
|
||||
info.state = 'active';
|
||||
info.activatedAt = Date.now();
|
||||
logger.debug(`Initialized: ${pluginId}`);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to initialize ${pluginId}:`, e);
|
||||
info.state = 'error';
|
||||
info.error = e as Error;
|
||||
}
|
||||
} else {
|
||||
info.state = 'active';
|
||||
info.activatedAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
this._initialized = true;
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const activeCount = Array.from(this._plugins.values())
|
||||
.filter(p => p.state === 'active').length;
|
||||
|
||||
logger.info(`Runtime initialized | 运行时初始化完成`, {
|
||||
active: activeCount,
|
||||
total: this._plugins.size,
|
||||
duration: `${duration}ms`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 为场景创建系统
|
||||
* @en Create systems for scene
|
||||
*
|
||||
* @param scene - @zh 场景 @en Scene
|
||||
* @param context - @zh 系统上下文 @en System context
|
||||
*/
|
||||
createSystemsForScene(scene: IScene, context: SystemContext): void {
|
||||
this._currentScene = scene;
|
||||
this._currentContext = context;
|
||||
|
||||
const sortedPlugins = this._topologicalSort();
|
||||
|
||||
// Phase 1: 创建系统 | Create systems
|
||||
for (const pluginId of sortedPlugins) {
|
||||
const info = this._plugins.get(pluginId);
|
||||
if (!info?.enabled || info.state === 'error') continue;
|
||||
|
||||
const mod = info.plugin.runtimeModule;
|
||||
if (mod?.createSystems) {
|
||||
try {
|
||||
const systemsBefore = scene.systems.length;
|
||||
mod.createSystems(scene, context);
|
||||
|
||||
// 跟踪创建的系统 | Track created systems
|
||||
const systemsAfter = scene.systems;
|
||||
info.systemInstances = [];
|
||||
for (let i = systemsBefore; i < systemsAfter.length; i++) {
|
||||
info.systemInstances.push(systemsAfter[i]);
|
||||
}
|
||||
|
||||
logger.debug(`Systems created for: ${pluginId}`, {
|
||||
count: info.systemInstances.length
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Failed to create systems for ${pluginId}:`, e);
|
||||
info.state = 'error';
|
||||
info.error = e as Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: 系统创建后回调 | Post-creation callbacks
|
||||
for (const pluginId of sortedPlugins) {
|
||||
const info = this._plugins.get(pluginId);
|
||||
if (!info?.enabled || info.state === 'error') continue;
|
||||
|
||||
const mod = info.plugin.runtimeModule;
|
||||
if (mod?.onSystemsCreated) {
|
||||
try {
|
||||
mod.onSystemsCreated(scene, context);
|
||||
logger.debug(`Systems wired for: ${pluginId}`);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to wire systems for ${pluginId}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Systems created for scene | 场景系统创建完成');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 查询 | Query
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 获取插件
|
||||
* @en Get plugin
|
||||
*/
|
||||
getPlugin(id: string): IRuntimePlugin | undefined {
|
||||
return this._plugins.get(id)?.plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取插件信息
|
||||
* @en Get plugin info
|
||||
*/
|
||||
getPluginInfo(id: string): RegisteredPluginInfo | undefined {
|
||||
return this._plugins.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有插件
|
||||
* @en Get all plugins
|
||||
*/
|
||||
getPlugins(): IRuntimePlugin[] {
|
||||
return Array.from(this._plugins.values()).map(p => p.plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有启用的插件
|
||||
* @en Get all enabled plugins
|
||||
*/
|
||||
getEnabledPlugins(): IRuntimePlugin[] {
|
||||
return Array.from(this._plugins.values())
|
||||
.filter(p => p.enabled)
|
||||
.map(p => p.plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取插件状态
|
||||
* @en Get plugin state
|
||||
*/
|
||||
getState(pluginId: string): PluginState | undefined {
|
||||
return this._plugins.get(pluginId)?.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取失败的插件
|
||||
* @en Get failed plugins
|
||||
*/
|
||||
getFailedPlugins(): Array<{ id: string; error: Error }> {
|
||||
const failed: Array<{ id: string; error: Error }> = [];
|
||||
for (const [id, info] of this._plugins) {
|
||||
if (info.state === 'error' && info.error) {
|
||||
failed.push({ id, error: info.error });
|
||||
}
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 是否已初始化
|
||||
* @en Whether initialized
|
||||
*/
|
||||
get initialized(): boolean {
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期 | Lifecycle
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 清理场景系统
|
||||
* @en Clear scene systems
|
||||
*/
|
||||
clearSceneSystems(): void {
|
||||
for (const [pluginId, info] of this._plugins) {
|
||||
if (!info.enabled) continue;
|
||||
|
||||
const mod = info.plugin.runtimeModule;
|
||||
if (mod?.onDestroy) {
|
||||
try {
|
||||
mod.onDestroy();
|
||||
} catch (e) {
|
||||
logger.error(`Error in ${pluginId}.onDestroy:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
info.systemInstances = [];
|
||||
}
|
||||
|
||||
this._currentScene = null;
|
||||
this._currentContext = null;
|
||||
logger.debug('Scene systems cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置管理器
|
||||
* @en Reset manager
|
||||
*/
|
||||
reset(): void {
|
||||
this.clearSceneSystems();
|
||||
this._plugins.clear();
|
||||
this._initialized = false;
|
||||
logger.info('PluginManager reset');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 私有方法 | Private Methods
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 拓扑排序(使用统一的 DependencyUtils)
|
||||
* @en Topological sort (using unified DependencyUtils)
|
||||
*/
|
||||
private _topologicalSort(): string[] {
|
||||
// 转换为 IDependable 格式
|
||||
const items: IDependable[] = Array.from(this._plugins.entries()).map(
|
||||
([id, info]) => ({
|
||||
id,
|
||||
dependencies: info.plugin.manifest.dependencies
|
||||
})
|
||||
);
|
||||
|
||||
const result = topologicalSort(items, {
|
||||
algorithm: 'dfs',
|
||||
resolveId: resolveDependencyId
|
||||
});
|
||||
|
||||
if (result.hasCycles) {
|
||||
logger.warn(`Circular dependencies detected: ${result.cycleIds?.join(', ')}`);
|
||||
}
|
||||
|
||||
return result.sorted.map(item => item.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取反向依赖(使用统一的 DependencyUtils)
|
||||
* @en Get reverse dependencies (using unified DependencyUtils)
|
||||
*/
|
||||
private _getReverseDependencies(pluginId: string): Set<string> {
|
||||
const items: IDependable[] = Array.from(this._plugins.entries()).map(
|
||||
([id, info]) => ({
|
||||
id,
|
||||
dependencies: info.plugin.manifest.dependencies
|
||||
})
|
||||
);
|
||||
|
||||
return getReverseDependencies(pluginId, items, {
|
||||
resolveId: resolveDependencyId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 全局运行时插件管理器实例
|
||||
* @en Global runtime plugin manager instance
|
||||
*/
|
||||
export const runtimePluginManager = new RuntimePluginManager();
|
||||
189
packages/engine/runtime-core/src/PluginState.ts
Normal file
189
packages/engine/runtime-core/src/PluginState.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* @zh 插件生命周期状态
|
||||
* @en Plugin Lifecycle State
|
||||
*
|
||||
* @zh 提供统一的插件状态定义,确保运行时和编辑器使用一致的状态机。
|
||||
* @en Provides unified plugin state definition, ensuring runtime and editor use consistent state machine.
|
||||
*
|
||||
* @zh 状态转换图 | State Transition Diagram:
|
||||
* ```
|
||||
* ┌──────────────────────────────────────────┐
|
||||
* │ │
|
||||
* ▼ │
|
||||
* ┌──────────┐ load ┌─────────┐ init ┌────────────┐ │
|
||||
* │ Unloaded │ ──────► │ Loading │ ──────► │Initializing│ │
|
||||
* └──────────┘ └─────────┘ └────────────┘ │
|
||||
* ▲ │ │ │
|
||||
* │ │ fail │ success │
|
||||
* │ ▼ ▼ │
|
||||
* │ ┌─────────┐ ┌─────────┐ │
|
||||
* │ │ Error │◄────────│ Active │ │
|
||||
* │ └─────────┘ error └─────────┘ │
|
||||
* │ │ │ │
|
||||
* │ │ retry │ disable │
|
||||
* │ ▼ ▼ │
|
||||
* │ ┌─────────┐ ┌──────────┐ │
|
||||
* └──────────────│ Loading │◄────────│ Disabled │─────┘
|
||||
* unload └─────────┘ enable └──────────┘
|
||||
* ```
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// 插件状态 | Plugin State
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 插件生命周期状态
|
||||
* @en Plugin lifecycle state
|
||||
*
|
||||
* @zh 统一定义,供 runtime-core 和 editor-core 共用
|
||||
* @en Unified definition for both runtime-core and editor-core
|
||||
*/
|
||||
export enum PluginLifecycleState {
|
||||
/**
|
||||
* @zh 未加载 - 初始状态
|
||||
* @en Unloaded - initial state
|
||||
*/
|
||||
Unloaded = 'unloaded',
|
||||
|
||||
/**
|
||||
* @zh 加载中 - 正在加载插件代码
|
||||
* @en Loading - loading plugin code
|
||||
*/
|
||||
Loading = 'loading',
|
||||
|
||||
/**
|
||||
* @zh 初始化中 - 正在执行初始化逻辑
|
||||
* @en Initializing - executing initialization logic
|
||||
*/
|
||||
Initializing = 'initializing',
|
||||
|
||||
/**
|
||||
* @zh 活动中 - 插件正常运行
|
||||
* @en Active - plugin running normally
|
||||
*/
|
||||
Active = 'active',
|
||||
|
||||
/**
|
||||
* @zh 错误 - 加载或运行时出错
|
||||
* @en Error - error during loading or runtime
|
||||
*/
|
||||
Error = 'error',
|
||||
|
||||
/**
|
||||
* @zh 已禁用 - 用户主动禁用
|
||||
* @en Disabled - user disabled
|
||||
*/
|
||||
Disabled = 'disabled'
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 插件状态(简化别名,向后兼容)
|
||||
* @en Plugin state (simplified alias for backward compatibility)
|
||||
*/
|
||||
export type PluginState =
|
||||
| 'unloaded'
|
||||
| 'loading'
|
||||
| 'initializing'
|
||||
| 'active'
|
||||
| 'error'
|
||||
| 'disabled';
|
||||
|
||||
// ============================================================================
|
||||
// 状态转换 | State Transitions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 有效的状态转换
|
||||
* @en Valid state transitions
|
||||
*/
|
||||
export const VALID_STATE_TRANSITIONS: Record<PluginState, PluginState[]> = {
|
||||
unloaded: ['loading'],
|
||||
loading: ['initializing', 'error', 'unloaded'],
|
||||
initializing: ['active', 'error'],
|
||||
active: ['disabled', 'error', 'unloaded'],
|
||||
error: ['loading', 'unloaded'],
|
||||
disabled: ['loading', 'unloaded']
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 检查状态转换是否有效
|
||||
* @en Check if state transition is valid
|
||||
*
|
||||
* @param from - @zh 当前状态 @en Current state
|
||||
* @param to - @zh 目标状态 @en Target state
|
||||
* @returns @zh 是否允许转换 @en Whether transition is allowed
|
||||
*/
|
||||
export function isValidStateTransition(from: PluginState, to: PluginState): boolean {
|
||||
return VALID_STATE_TRANSITIONS[from]?.includes(to) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查插件是否可操作
|
||||
* @en Check if plugin is operable
|
||||
*/
|
||||
export function isPluginOperable(state: PluginState): boolean {
|
||||
return state === 'active' || state === 'disabled';
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查插件是否正在加载
|
||||
* @en Check if plugin is loading
|
||||
*/
|
||||
export function isPluginLoading(state: PluginState): boolean {
|
||||
return state === 'loading' || state === 'initializing';
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查插件是否可用
|
||||
* @en Check if plugin is available
|
||||
*/
|
||||
export function isPluginAvailable(state: PluginState): boolean {
|
||||
return state === 'active';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 状态转换事件 | State Transition Events
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 状态转换事件
|
||||
* @en State transition event
|
||||
*/
|
||||
export interface PluginStateChangeEvent {
|
||||
/**
|
||||
* @zh 插件 ID
|
||||
* @en Plugin ID
|
||||
*/
|
||||
pluginId: string;
|
||||
|
||||
/**
|
||||
* @zh 之前的状态
|
||||
* @en Previous state
|
||||
*/
|
||||
previousState: PluginState;
|
||||
|
||||
/**
|
||||
* @zh 当前状态
|
||||
* @en Current state
|
||||
*/
|
||||
currentState: PluginState;
|
||||
|
||||
/**
|
||||
* @zh 错误信息(如果有)
|
||||
* @en Error message (if any)
|
||||
*/
|
||||
error?: Error;
|
||||
|
||||
/**
|
||||
* @zh 时间戳
|
||||
* @en Timestamp
|
||||
*/
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 状态变更监听器
|
||||
* @en State change listener
|
||||
*/
|
||||
export type PluginStateChangeListener = (event: PluginStateChangeEvent) => void;
|
||||
200
packages/engine/runtime-core/src/ProjectConfig.ts
Normal file
200
packages/engine/runtime-core/src/ProjectConfig.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import type { PluginPackageInfo, PluginConfig, PluginLoadConfig } from './PluginLoader';
|
||||
|
||||
export interface ProjectConfig {
|
||||
name: string;
|
||||
version: string;
|
||||
plugins: Record<string, PluginConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 扩展的插件包信息(包含依赖)
|
||||
* @en Extended plugin package info (with dependencies)
|
||||
*/
|
||||
export interface ExtendedPluginPackageInfo extends PluginPackageInfo {
|
||||
/**
|
||||
* @zh 依赖的包 ID 列表
|
||||
* @en List of dependency package IDs
|
||||
*/
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 内置引擎插件的包信息(包含依赖关系)
|
||||
* @en Built-in engine plugin package info (with dependencies)
|
||||
*
|
||||
* @zh 依赖顺序很重要,确保插件按正确顺序加载
|
||||
* @en Dependency order matters, ensures plugins load in correct order
|
||||
*/
|
||||
export const BUILTIN_PLUGIN_PACKAGES: Record<string, ExtendedPluginPackageInfo> = {
|
||||
'@esengine/engine-core': {
|
||||
plugin: true,
|
||||
pluginExport: 'EnginePlugin',
|
||||
category: 'core',
|
||||
isEnginePlugin: true,
|
||||
dependencies: []
|
||||
},
|
||||
'@esengine/camera': {
|
||||
plugin: true,
|
||||
pluginExport: 'CameraPlugin',
|
||||
category: 'core',
|
||||
isEnginePlugin: true,
|
||||
dependencies: ['@esengine/engine-core']
|
||||
},
|
||||
'@esengine/sprite': {
|
||||
plugin: true,
|
||||
pluginExport: 'SpritePlugin',
|
||||
category: 'rendering',
|
||||
isEnginePlugin: true,
|
||||
dependencies: ['@esengine/engine-core']
|
||||
},
|
||||
'@esengine/audio': {
|
||||
plugin: true,
|
||||
pluginExport: 'AudioPlugin',
|
||||
category: 'audio',
|
||||
isEnginePlugin: true,
|
||||
dependencies: ['@esengine/engine-core']
|
||||
},
|
||||
'@esengine/ui': {
|
||||
plugin: true,
|
||||
pluginExport: 'UIPlugin',
|
||||
category: 'ui',
|
||||
dependencies: ['@esengine/engine-core', '@esengine/sprite']
|
||||
},
|
||||
'@esengine/fairygui': {
|
||||
plugin: true,
|
||||
pluginExport: 'FGUIPlugin',
|
||||
category: 'ui',
|
||||
dependencies: ['@esengine/engine-core', '@esengine/sprite']
|
||||
},
|
||||
'@esengine/tilemap': {
|
||||
plugin: true,
|
||||
pluginExport: 'TilemapPlugin',
|
||||
category: 'tilemap',
|
||||
dependencies: ['@esengine/engine-core', '@esengine/sprite']
|
||||
},
|
||||
'@esengine/behavior-tree': {
|
||||
plugin: true,
|
||||
pluginExport: 'BehaviorTreePlugin',
|
||||
category: 'ai',
|
||||
dependencies: ['@esengine/engine-core']
|
||||
},
|
||||
'@esengine/physics-rapier2d': {
|
||||
plugin: true,
|
||||
pluginExport: 'PhysicsPlugin',
|
||||
category: 'physics',
|
||||
dependencies: ['@esengine/engine-core']
|
||||
},
|
||||
'@esengine/particle': {
|
||||
plugin: true,
|
||||
pluginExport: 'ParticlePlugin',
|
||||
category: 'rendering',
|
||||
dependencies: ['@esengine/engine-core', '@esengine/sprite']
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 将项目配置转换为 UnifiedPluginLoader 配置
|
||||
* @en Convert project config to UnifiedPluginLoader config
|
||||
*
|
||||
* @param config - @zh 项目配置 @en Project config
|
||||
* @param packageInfoMap - @zh 包信息映射 @en Package info map
|
||||
* @returns @zh 插件加载配置列表 @en Plugin load config list
|
||||
*/
|
||||
export function convertToPluginLoadConfigs(
|
||||
config: ProjectConfig,
|
||||
packageInfoMap: Record<string, ExtendedPluginPackageInfo> = BUILTIN_PLUGIN_PACKAGES
|
||||
): PluginLoadConfig[] {
|
||||
const result: PluginLoadConfig[] = [];
|
||||
|
||||
for (const [packageId, pluginConfig] of Object.entries(config.plugins)) {
|
||||
const packageInfo = packageInfoMap[packageId];
|
||||
if (!packageInfo) {
|
||||
console.warn(`[ProjectConfig] No package info for ${packageId}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
packageId,
|
||||
enabled: pluginConfig.enabled,
|
||||
sourceType: 'npm',
|
||||
exportName: packageInfo.pluginExport,
|
||||
dependencies: packageInfo.dependencies,
|
||||
options: pluginConfig.options
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建默认项目配置
|
||||
* @en Create default project config
|
||||
*/
|
||||
export function createDefaultProjectConfig(): ProjectConfig {
|
||||
return {
|
||||
name: 'New Project',
|
||||
version: '1.0.0',
|
||||
plugins: {
|
||||
'@esengine/engine-core': { enabled: true },
|
||||
'@esengine/camera': { enabled: true },
|
||||
'@esengine/sprite': { enabled: true },
|
||||
'@esengine/audio': { enabled: true },
|
||||
'@esengine/ui': { enabled: true },
|
||||
'@esengine/particle': { enabled: false },
|
||||
'@esengine/fairygui': { enabled: false },
|
||||
'@esengine/tilemap': { enabled: false },
|
||||
'@esengine/behavior-tree': { enabled: false },
|
||||
'@esengine/physics-rapier2d': { enabled: false }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并用户配置与默认配置
|
||||
*/
|
||||
export function mergeProjectConfig(
|
||||
userConfig: Partial<ProjectConfig>
|
||||
): ProjectConfig {
|
||||
const defaultConfig = createDefaultProjectConfig();
|
||||
|
||||
return {
|
||||
name: userConfig.name || defaultConfig.name,
|
||||
version: userConfig.version || defaultConfig.version,
|
||||
plugins: {
|
||||
...defaultConfig.plugins,
|
||||
...userConfig.plugins
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从编辑器的 enabledPlugins 列表创建项目配置
|
||||
* Create project config from editor's enabledPlugins list
|
||||
*
|
||||
* @param enabledPlugins - 启用的插件 ID 列表 / List of enabled plugin IDs
|
||||
*/
|
||||
export function createProjectConfigFromEnabledList(
|
||||
enabledPlugins: string[]
|
||||
): ProjectConfig {
|
||||
const defaultConfig = createDefaultProjectConfig();
|
||||
|
||||
// 先禁用所有非核心插件
|
||||
// First disable all non-core plugins
|
||||
const plugins: Record<string, PluginConfig> = {};
|
||||
|
||||
for (const id of Object.keys(defaultConfig.plugins)) {
|
||||
const packageInfo = BUILTIN_PLUGIN_PACKAGES[id];
|
||||
// 核心插件始终启用
|
||||
// Core plugins are always enabled
|
||||
if (packageInfo?.isEnginePlugin) {
|
||||
plugins[id] = { enabled: true };
|
||||
} else {
|
||||
plugins[id] = { enabled: enabledPlugins.includes(id) };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultConfig,
|
||||
plugins
|
||||
};
|
||||
}
|
||||
66
packages/engine/runtime-core/src/RuntimeBootstrap.ts
Normal file
66
packages/engine/runtime-core/src/RuntimeBootstrap.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Runtime Bootstrap
|
||||
* 运行时启动器 - 提供通用的初始化流程
|
||||
*/
|
||||
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import type { IScene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
runtimePluginManager,
|
||||
type IRuntimePlugin,
|
||||
type IRuntimeModule,
|
||||
type ModuleManifest,
|
||||
type SystemContext
|
||||
} from './PluginManager';
|
||||
|
||||
export interface RuntimeConfig {
|
||||
enabledPlugins?: string[];
|
||||
isEditor?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建插件(简化工厂)
|
||||
*/
|
||||
export function createPlugin(
|
||||
manifest: ModuleManifest,
|
||||
runtimeModule: IRuntimeModule
|
||||
): IRuntimePlugin {
|
||||
return { manifest, runtimeModule };
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册插件到运行时
|
||||
*/
|
||||
export function registerPlugin(plugin: IRuntimePlugin): void {
|
||||
runtimePluginManager.register(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化运行时
|
||||
* @param config 运行时配置
|
||||
*/
|
||||
export async function initializeRuntime(config?: RuntimeConfig): Promise<void> {
|
||||
if (config?.enabledPlugins) {
|
||||
runtimePluginManager.loadConfig({ enabledPlugins: config.enabledPlugins });
|
||||
} else {
|
||||
for (const plugin of runtimePluginManager.getPlugins()) {
|
||||
runtimePluginManager.enable(plugin.manifest.id);
|
||||
}
|
||||
}
|
||||
|
||||
await runtimePluginManager.initializeRuntime(Core.services);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为场景创建系统
|
||||
*/
|
||||
export function createSystemsForScene(scene: IScene, context: SystemContext): void {
|
||||
runtimePluginManager.createSystemsForScene(scene, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置运行时(用于热重载等场景)
|
||||
*/
|
||||
export function resetRuntime(): void {
|
||||
runtimePluginManager.reset();
|
||||
}
|
||||
190
packages/engine/runtime-core/src/RuntimeMode.ts
Normal file
190
packages/engine/runtime-core/src/RuntimeMode.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* @zh 运行时模式枚举
|
||||
* @en Runtime mode enumeration
|
||||
*
|
||||
* @zh 定义游戏运行时的不同运行模式,每种模式有不同的系统启用策略
|
||||
* @en Defines different runtime modes with different system enabling strategies
|
||||
*/
|
||||
export enum RuntimeMode {
|
||||
/**
|
||||
* @zh 编辑器静态模式 - 场景编辑状态
|
||||
* @en Editor static mode - scene editing state
|
||||
*
|
||||
* @zh 特性:
|
||||
* - 所有游戏逻辑系统禁用(物理、行为树、动画)
|
||||
* - 显示编辑器 UI(Grid、Gizmo、坐标轴)
|
||||
* - 组件生命周期回调被延迟(onAwake/onStart 不触发)
|
||||
* - 输入系统禁用(避免与编辑器操作冲突)
|
||||
*
|
||||
* @en Features:
|
||||
* - All game logic systems disabled (physics, behavior tree, animation)
|
||||
* - Editor UI visible (grid, gizmos, axis indicator)
|
||||
* - Component lifecycle callbacks deferred (onAwake/onStart not triggered)
|
||||
* - Input system disabled (avoid conflict with editor operations)
|
||||
*/
|
||||
EditorStatic = 'editor-static',
|
||||
|
||||
/**
|
||||
* @zh 编辑器预览模式 - 在编辑器中播放游戏
|
||||
* @en Editor preview mode - play game within editor
|
||||
*
|
||||
* @zh 特性:
|
||||
* - 游戏逻辑系统启用(物理、行为树、动画)
|
||||
* - 可选显示 Gizmo(用于调试)
|
||||
* - 组件生命周期回调触发
|
||||
* - 输入系统可选启用
|
||||
* - 场景快照用于恢复
|
||||
*
|
||||
* @en Features:
|
||||
* - Game logic systems enabled (physics, behavior tree, animation)
|
||||
* - Gizmos optionally visible (for debugging)
|
||||
* - Component lifecycle callbacks triggered
|
||||
* - Input system optionally enabled
|
||||
* - Scene snapshot for restoration
|
||||
*/
|
||||
EditorPreview = 'editor-preview',
|
||||
|
||||
/**
|
||||
* @zh 独立运行模式 - Web/桌面/小程序完整运行
|
||||
* @en Standalone mode - full Web/desktop/mini-program runtime
|
||||
*
|
||||
* @zh 特性:
|
||||
* - 所有系统启用
|
||||
* - 无编辑器 UI
|
||||
* - 完整的输入处理
|
||||
* - 生产环境配置
|
||||
*
|
||||
* @en Features:
|
||||
* - All systems enabled
|
||||
* - No editor UI
|
||||
* - Full input handling
|
||||
* - Production configuration
|
||||
*/
|
||||
Standalone = 'standalone'
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 运行模式配置
|
||||
* @en Runtime mode configuration
|
||||
*/
|
||||
export interface RuntimeModeConfig {
|
||||
/**
|
||||
* @zh 是否启用物理系统
|
||||
* @en Whether to enable physics system
|
||||
*/
|
||||
enablePhysics: boolean;
|
||||
|
||||
/**
|
||||
* @zh 是否启用行为树系统
|
||||
* @en Whether to enable behavior tree system
|
||||
*/
|
||||
enableBehaviorTree: boolean;
|
||||
|
||||
/**
|
||||
* @zh 是否启用动画系统
|
||||
* @en Whether to enable animation system
|
||||
*/
|
||||
enableAnimation: boolean;
|
||||
|
||||
/**
|
||||
* @zh 是否启用输入系统
|
||||
* @en Whether to enable input system
|
||||
*/
|
||||
enableInput: boolean;
|
||||
|
||||
/**
|
||||
* @zh 是否显示网格
|
||||
* @en Whether to show grid
|
||||
*/
|
||||
showGrid: boolean;
|
||||
|
||||
/**
|
||||
* @zh 是否显示 Gizmo
|
||||
* @en Whether to show gizmos
|
||||
*/
|
||||
showGizmos: boolean;
|
||||
|
||||
/**
|
||||
* @zh 是否显示坐标轴指示器
|
||||
* @en Whether to show axis indicator
|
||||
*/
|
||||
showAxisIndicator: boolean;
|
||||
|
||||
/**
|
||||
* @zh 是否触发组件生命周期回调
|
||||
* @en Whether to trigger component lifecycle callbacks
|
||||
*/
|
||||
triggerLifecycle: boolean;
|
||||
|
||||
/**
|
||||
* @zh 是否为编辑器环境(影响资产加载等)
|
||||
* @en Whether in editor environment (affects asset loading, etc.)
|
||||
*/
|
||||
isEditorEnvironment: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取指定模式的默认配置
|
||||
* @en Get default configuration for specified mode
|
||||
*
|
||||
* @param mode - @zh 运行模式 @en Runtime mode
|
||||
* @returns @zh 模式配置 @en Mode configuration
|
||||
*/
|
||||
export function getRuntimeModeConfig(mode: RuntimeMode): RuntimeModeConfig {
|
||||
switch (mode) {
|
||||
case RuntimeMode.EditorStatic:
|
||||
return {
|
||||
enablePhysics: false,
|
||||
enableBehaviorTree: false,
|
||||
enableAnimation: false,
|
||||
enableInput: false,
|
||||
showGrid: true,
|
||||
showGizmos: true,
|
||||
showAxisIndicator: true,
|
||||
triggerLifecycle: false,
|
||||
isEditorEnvironment: true
|
||||
};
|
||||
|
||||
case RuntimeMode.EditorPreview:
|
||||
return {
|
||||
enablePhysics: true,
|
||||
enableBehaviorTree: true,
|
||||
enableAnimation: true,
|
||||
enableInput: true,
|
||||
showGrid: false,
|
||||
showGizmos: false, // 预览时默认隐藏,可通过设置开启
|
||||
showAxisIndicator: false,
|
||||
triggerLifecycle: true,
|
||||
isEditorEnvironment: true
|
||||
};
|
||||
|
||||
case RuntimeMode.Standalone:
|
||||
return {
|
||||
enablePhysics: true,
|
||||
enableBehaviorTree: true,
|
||||
enableAnimation: true,
|
||||
enableInput: true,
|
||||
showGrid: false,
|
||||
showGizmos: false,
|
||||
showAxisIndicator: false,
|
||||
triggerLifecycle: true,
|
||||
isEditorEnvironment: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查模式是否为编辑器模式
|
||||
* @en Check if mode is an editor mode
|
||||
*/
|
||||
export function isEditorMode(mode: RuntimeMode): boolean {
|
||||
return mode === RuntimeMode.EditorStatic || mode === RuntimeMode.EditorPreview;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查模式是否应启用游戏逻辑
|
||||
* @en Check if mode should enable game logic
|
||||
*/
|
||||
export function shouldEnableGameLogic(mode: RuntimeMode): boolean {
|
||||
return mode === RuntimeMode.EditorPreview || mode === RuntimeMode.Standalone;
|
||||
}
|
||||
641
packages/engine/runtime-core/src/UserCodeRealm.ts
Normal file
641
packages/engine/runtime-core/src/UserCodeRealm.ts
Normal file
@@ -0,0 +1,641 @@
|
||||
/**
|
||||
* @zh 用户代码隔离域
|
||||
* @en User Code Realm
|
||||
*
|
||||
* @zh 提供用户代码(组件、系统、服务)与引擎核心的隔离。
|
||||
* @en Provides isolation between user code (components, systems, services) and engine core.
|
||||
*
|
||||
* @zh 设计目标 | Design goals:
|
||||
* @en
|
||||
* 1. 隔离注册 - 用户组件/系统不污染引擎核心注册表
|
||||
* 2. 干净卸载 - 切换项目或热更新时可完全清理用户代码
|
||||
* 3. 类型安全 - 使用 ServiceToken 提供类型安全的服务访问
|
||||
* 4. 热更新友好 - 支持组件/系统的原地更新
|
||||
*/
|
||||
|
||||
import type { Component, EntitySystem, IScene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
ComponentRegistry,
|
||||
GlobalComponentRegistry,
|
||||
PluginServiceRegistry,
|
||||
createServiceToken,
|
||||
type ServiceToken
|
||||
} from '@esengine/ecs-framework';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('UserCodeRealm');
|
||||
|
||||
/**
|
||||
* @zh 用户代码隔离域配置
|
||||
* @en User Code Realm configuration
|
||||
*/
|
||||
export interface UserCodeRealmConfig {
|
||||
/**
|
||||
* @zh 是否启用热更新模式
|
||||
* @en Whether to enable hot reload mode
|
||||
* @default true
|
||||
*/
|
||||
hotReloadEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* @zh 是否在初始化时从全局注册表继承组件
|
||||
* @en Whether to inherit components from global registry on initialization
|
||||
* @default true
|
||||
*/
|
||||
inheritGlobalComponents?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 已注册的用户系统信息
|
||||
* @en Registered user system info
|
||||
*/
|
||||
export interface UserSystemInfo {
|
||||
/**
|
||||
* @zh 系统名称
|
||||
* @en System name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* @zh 系统类
|
||||
* @en System class
|
||||
*/
|
||||
systemClass: new (...args: unknown[]) => EntitySystem;
|
||||
|
||||
/**
|
||||
* @zh 系统实例
|
||||
* @en System instance
|
||||
*/
|
||||
instance: EntitySystem;
|
||||
|
||||
/**
|
||||
* @zh 系统所属场景
|
||||
* @en Scene the system belongs to
|
||||
*/
|
||||
scene: IScene;
|
||||
|
||||
/**
|
||||
* @zh 更新顺序
|
||||
* @en Update order
|
||||
*/
|
||||
updateOrder: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 已注册的用户组件信息
|
||||
* @en Registered user component info
|
||||
*/
|
||||
export interface UserComponentInfo {
|
||||
/**
|
||||
* @zh 组件名称
|
||||
* @en Component name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* @zh 组件类
|
||||
* @en Component class
|
||||
*/
|
||||
componentClass: new (...args: unknown[]) => Component;
|
||||
|
||||
/**
|
||||
* @zh 分配的位索引
|
||||
* @en Allocated bit index
|
||||
*/
|
||||
bitIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 用户代码隔离域
|
||||
* @en User Code Realm
|
||||
*
|
||||
* @zh 管理用户定义的组件、系统和服务,提供与引擎核心的隔离。
|
||||
* @en Manages user-defined components, systems, and services with isolation from engine core.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const realm = new UserCodeRealm();
|
||||
*
|
||||
* // 注册用户组件 | Register user component
|
||||
* realm.registerComponent(MyComponent);
|
||||
*
|
||||
* // 创建用户系统实例 | Create user system instance
|
||||
* const system = realm.createSystem(MySystem, scene);
|
||||
*
|
||||
* // 注册用户服务 | Register user service
|
||||
* realm.registerService(MyServiceToken, myServiceInstance);
|
||||
*
|
||||
* // 清理所有用户代码 | Clean up all user code
|
||||
* realm.dispose();
|
||||
* ```
|
||||
*/
|
||||
export class UserCodeRealm {
|
||||
/**
|
||||
* @zh 用户组件注册表
|
||||
* @en User component registry
|
||||
*/
|
||||
private _componentRegistry: ComponentRegistry;
|
||||
|
||||
/**
|
||||
* @zh 用户服务注册表
|
||||
* @en User service registry
|
||||
*/
|
||||
private _serviceRegistry: PluginServiceRegistry;
|
||||
|
||||
/**
|
||||
* @zh 已注册的用户系统
|
||||
* @en Registered user systems
|
||||
*/
|
||||
private _systems: UserSystemInfo[] = [];
|
||||
|
||||
/**
|
||||
* @zh 已注册的用户组件信息
|
||||
* @en Registered user component info
|
||||
*/
|
||||
private _components: Map<string, UserComponentInfo> = new Map();
|
||||
|
||||
/**
|
||||
* @zh 配置
|
||||
* @en Configuration
|
||||
*/
|
||||
private _config: Required<UserCodeRealmConfig>;
|
||||
|
||||
/**
|
||||
* @zh 是否已释放
|
||||
* @en Whether disposed
|
||||
*/
|
||||
private _disposed = false;
|
||||
|
||||
/**
|
||||
* @zh 创建用户代码隔离域
|
||||
* @en Create user code realm
|
||||
*
|
||||
* @param config - @zh 配置选项 @en Configuration options
|
||||
*/
|
||||
constructor(config?: UserCodeRealmConfig) {
|
||||
this._config = {
|
||||
hotReloadEnabled: config?.hotReloadEnabled ?? true,
|
||||
inheritGlobalComponents: config?.inheritGlobalComponents ?? true
|
||||
};
|
||||
|
||||
this._componentRegistry = new ComponentRegistry();
|
||||
this._serviceRegistry = new PluginServiceRegistry();
|
||||
|
||||
if (this._config.hotReloadEnabled) {
|
||||
this._componentRegistry.enableHotReload();
|
||||
}
|
||||
|
||||
if (this._config.inheritGlobalComponents) {
|
||||
this._componentRegistry.cloneFrom(GlobalComponentRegistry);
|
||||
}
|
||||
|
||||
logger.debug('UserCodeRealm created', {
|
||||
hotReloadEnabled: this._config.hotReloadEnabled,
|
||||
inheritGlobalComponents: this._config.inheritGlobalComponents
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 组件管理 | Component Management
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 注册用户组件类
|
||||
* @en Register user component class
|
||||
*
|
||||
* @param componentClass - @zh 组件类 @en Component class
|
||||
* @returns @zh 分配的位索引 @en Allocated bit index
|
||||
*/
|
||||
registerComponent<T extends Component>(
|
||||
componentClass: new (...args: unknown[]) => T
|
||||
): number {
|
||||
this._ensureNotDisposed();
|
||||
|
||||
const name = componentClass.name;
|
||||
const bitIndex = this._componentRegistry.register(componentClass as any);
|
||||
|
||||
// 同时注册到全局注册表(用于序列化/反序列化)
|
||||
// Also register to global registry (for serialization/deserialization)
|
||||
try {
|
||||
GlobalComponentRegistry.register(componentClass as any);
|
||||
} catch {
|
||||
// 已注册则忽略 | Ignore if already registered
|
||||
}
|
||||
|
||||
this._components.set(name, {
|
||||
name,
|
||||
componentClass,
|
||||
bitIndex
|
||||
});
|
||||
|
||||
logger.debug(`Component registered: ${name}`, { bitIndex });
|
||||
|
||||
return bitIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注销用户组件
|
||||
* @en Unregister user component
|
||||
*
|
||||
* @param componentName - @zh 组件名称 @en Component name
|
||||
*/
|
||||
unregisterComponent(componentName: string): void {
|
||||
this._ensureNotDisposed();
|
||||
|
||||
this._componentRegistry.unregister(componentName);
|
||||
this._components.delete(componentName);
|
||||
|
||||
logger.debug(`Component unregistered: ${componentName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取用户组件类
|
||||
* @en Get user component class
|
||||
*
|
||||
* @param componentName - @zh 组件名称 @en Component name
|
||||
* @returns @zh 组件类或 undefined @en Component class or undefined
|
||||
*/
|
||||
getComponent(componentName: string): UserComponentInfo | undefined {
|
||||
return this._components.get(componentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有已注册的用户组件
|
||||
* @en Get all registered user components
|
||||
*/
|
||||
getAllComponents(): UserComponentInfo[] {
|
||||
return Array.from(this._components.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取用户组件注册表
|
||||
* @en Get user component registry
|
||||
*/
|
||||
get componentRegistry(): ComponentRegistry {
|
||||
return this._componentRegistry;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 系统管理 | System Management
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建并注册用户系统
|
||||
* @en Create and register user system
|
||||
*
|
||||
* @param systemClass - @zh 系统类 @en System class
|
||||
* @param scene - @zh 目标场景 @en Target scene
|
||||
* @param updateOrder - @zh 更新顺序 @en Update order
|
||||
* @returns @zh 创建的系统实例 @en Created system instance
|
||||
*/
|
||||
createSystem<T extends EntitySystem>(
|
||||
systemClass: new (...args: unknown[]) => T,
|
||||
scene: IScene,
|
||||
updateOrder = 0
|
||||
): T {
|
||||
this._ensureNotDisposed();
|
||||
|
||||
const instance = new systemClass();
|
||||
const name = systemClass.name;
|
||||
|
||||
// 设置系统属性 | Set system properties
|
||||
if ('updateOrder' in instance) {
|
||||
(instance as any).updateOrder = updateOrder;
|
||||
}
|
||||
|
||||
// 添加到场景 | Add to scene
|
||||
scene.addSystem(instance);
|
||||
|
||||
// 记录系统信息 | Record system info
|
||||
this._systems.push({
|
||||
name,
|
||||
systemClass,
|
||||
instance,
|
||||
scene,
|
||||
updateOrder
|
||||
});
|
||||
|
||||
logger.debug(`System created: ${name}`, { updateOrder });
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 移除用户系统
|
||||
* @en Remove user system
|
||||
*
|
||||
* @param system - @zh 系统实例 @en System instance
|
||||
*/
|
||||
removeSystem(system: EntitySystem): void {
|
||||
this._ensureNotDisposed();
|
||||
|
||||
const index = this._systems.findIndex(s => s.instance === system);
|
||||
if (index !== -1) {
|
||||
const info = this._systems[index];
|
||||
|
||||
// 从场景移除 | Remove from scene
|
||||
try {
|
||||
info.scene.removeSystem(system);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to remove system from scene: ${info.name}`, err);
|
||||
}
|
||||
|
||||
this._systems.splice(index, 1);
|
||||
logger.debug(`System removed: ${info.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 移除场景的所有用户系统
|
||||
* @en Remove all user systems from a scene
|
||||
*
|
||||
* @param scene - @zh 目标场景 @en Target scene
|
||||
*/
|
||||
removeSystemsFromScene(scene: IScene): void {
|
||||
this._ensureNotDisposed();
|
||||
|
||||
const toRemove = this._systems.filter(s => s.scene === scene);
|
||||
|
||||
for (const info of toRemove) {
|
||||
try {
|
||||
scene.removeSystem(info.instance);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to remove system from scene: ${info.name}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
this._systems = this._systems.filter(s => s.scene !== scene);
|
||||
|
||||
logger.debug(`Removed ${toRemove.length} systems from scene`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有用户系统
|
||||
* @en Get all user systems
|
||||
*/
|
||||
getAllSystems(): UserSystemInfo[] {
|
||||
return [...this._systems];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取场景的用户系统
|
||||
* @en Get user systems of a scene
|
||||
*
|
||||
* @param scene - @zh 目标场景 @en Target scene
|
||||
*/
|
||||
getSystemsForScene(scene: IScene): UserSystemInfo[] {
|
||||
return this._systems.filter(s => s.scene === scene);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 服务管理 | Service Management
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 注册用户服务
|
||||
* @en Register user service
|
||||
*
|
||||
* @param token - @zh 服务令牌 @en Service token
|
||||
* @param service - @zh 服务实例 @en Service instance
|
||||
*/
|
||||
registerService<T>(token: ServiceToken<T>, service: T): void {
|
||||
this._ensureNotDisposed();
|
||||
this._serviceRegistry.register(token, service);
|
||||
logger.debug(`Service registered: ${token.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取用户服务
|
||||
* @en Get user service
|
||||
*
|
||||
* @param token - @zh 服务令牌 @en Service token
|
||||
* @returns @zh 服务实例或 undefined @en Service instance or undefined
|
||||
*/
|
||||
getService<T>(token: ServiceToken<T>): T | undefined {
|
||||
return this._serviceRegistry.get(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取用户服务(必需)
|
||||
* @en Get user service (required)
|
||||
*
|
||||
* @param token - @zh 服务令牌 @en Service token
|
||||
* @throws @zh 如果服务未注册 @en If service not registered
|
||||
*/
|
||||
requireService<T>(token: ServiceToken<T>): T {
|
||||
return this._serviceRegistry.require(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查服务是否已注册
|
||||
* @en Check if service is registered
|
||||
*
|
||||
* @param token - @zh 服务令牌 @en Service token
|
||||
*/
|
||||
hasService<T>(token: ServiceToken<T>): boolean {
|
||||
return this._serviceRegistry.has(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 注销用户服务
|
||||
* @en Unregister user service
|
||||
*
|
||||
* @param token - @zh 服务令牌 @en Service token
|
||||
*/
|
||||
unregisterService<T>(token: ServiceToken<T>): boolean {
|
||||
const result = this._serviceRegistry.unregister(token);
|
||||
if (result) {
|
||||
logger.debug(`Service unregistered: ${token.name}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取用户服务注册表
|
||||
* @en Get user service registry
|
||||
*/
|
||||
get serviceRegistry(): PluginServiceRegistry {
|
||||
return this._serviceRegistry;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 热更新 | Hot Reload
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 热更新组件类
|
||||
* @en Hot reload component class
|
||||
*
|
||||
* @zh 更新已注册组件的类定义,保持位索引不变。
|
||||
* @en Update registered component class definition while keeping bit index unchanged.
|
||||
*
|
||||
* @param componentClass - @zh 新的组件类 @en New component class
|
||||
* @returns @zh 是否成功更新 @en Whether update succeeded
|
||||
*/
|
||||
hotReloadComponent<T extends Component>(
|
||||
componentClass: new (...args: unknown[]) => T
|
||||
): boolean {
|
||||
this._ensureNotDisposed();
|
||||
|
||||
if (!this._config.hotReloadEnabled) {
|
||||
logger.warn('Hot reload is disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
const name = componentClass.name;
|
||||
const existing = this._components.get(name);
|
||||
|
||||
if (!existing) {
|
||||
// 新组件,直接注册 | New component, register directly
|
||||
this.registerComponent(componentClass);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 复用位索引,更新类引用 | Reuse bit index, update class reference
|
||||
const bitIndex = this._componentRegistry.register(componentClass as any);
|
||||
this._components.set(name, {
|
||||
name,
|
||||
componentClass,
|
||||
bitIndex
|
||||
});
|
||||
|
||||
// 更新全局注册表 | Update global registry
|
||||
try {
|
||||
GlobalComponentRegistry.register(componentClass as any);
|
||||
} catch {
|
||||
// 忽略 | Ignore
|
||||
}
|
||||
|
||||
logger.debug(`Component hot reloaded: ${name}`, { bitIndex });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 热更新系统
|
||||
* @en Hot reload systems
|
||||
*
|
||||
* @zh 移除旧系统实例,创建新系统实例。
|
||||
* @en Remove old system instances and create new ones.
|
||||
*
|
||||
* @param systemClasses - @zh 新的系统类列表 @en New system class list
|
||||
* @param scene - @zh 目标场景 @en Target scene
|
||||
* @returns @zh 新创建的系统实例 @en Newly created system instances
|
||||
*/
|
||||
hotReloadSystems<T extends EntitySystem>(
|
||||
systemClasses: Array<new (...args: unknown[]) => T>,
|
||||
scene: IScene
|
||||
): T[] {
|
||||
this._ensureNotDisposed();
|
||||
|
||||
// 移除场景的旧系统 | Remove old systems from scene
|
||||
this.removeSystemsFromScene(scene);
|
||||
|
||||
// 创建新系统 | Create new systems
|
||||
const newSystems: T[] = [];
|
||||
for (const systemClass of systemClasses) {
|
||||
const metadata = (systemClass as any).__systemMetadata__;
|
||||
const updateOrder = metadata?.updateOrder ?? 0;
|
||||
|
||||
const system = this.createSystem(systemClass, scene, updateOrder);
|
||||
newSystems.push(system);
|
||||
}
|
||||
|
||||
logger.info(`Hot reloaded ${newSystems.length} systems`);
|
||||
return newSystems;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期 | Lifecycle
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 重置隔离域
|
||||
* @en Reset the realm
|
||||
*
|
||||
* @zh 清除所有用户组件、系统和服务,但不释放隔离域。
|
||||
* @en Clear all user components, systems, and services without disposing the realm.
|
||||
*/
|
||||
reset(): void {
|
||||
this._ensureNotDisposed();
|
||||
|
||||
// 移除所有系统 | Remove all systems
|
||||
for (const info of this._systems) {
|
||||
try {
|
||||
info.scene.removeSystem(info.instance);
|
||||
} catch {
|
||||
// 忽略错误 | Ignore errors
|
||||
}
|
||||
}
|
||||
this._systems = [];
|
||||
|
||||
// 清除组件记录(不重置注册表,保持引擎组件)
|
||||
// Clear component records (don't reset registry, keep engine components)
|
||||
this._components.clear();
|
||||
|
||||
// 清除服务 | Clear services
|
||||
this._serviceRegistry.clear();
|
||||
|
||||
logger.info('UserCodeRealm reset');
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 释放隔离域
|
||||
* @en Dispose the realm
|
||||
*
|
||||
* @zh 完全清理所有资源。释放后隔离域不可再使用。
|
||||
* @en Completely clean up all resources. Realm cannot be used after disposal.
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 移除所有系统 | Remove all systems
|
||||
for (const info of this._systems) {
|
||||
try {
|
||||
info.scene.removeSystem(info.instance);
|
||||
} catch {
|
||||
// 忽略错误 | Ignore errors
|
||||
}
|
||||
}
|
||||
this._systems = [];
|
||||
|
||||
// 清除组件 | Clear components
|
||||
this._components.clear();
|
||||
this._componentRegistry.reset();
|
||||
|
||||
// 清除服务 | Clear services
|
||||
this._serviceRegistry.dispose();
|
||||
|
||||
this._disposed = true;
|
||||
logger.info('UserCodeRealm disposed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否已释放
|
||||
* @en Check if disposed
|
||||
*/
|
||||
get isDisposed(): boolean {
|
||||
return this._disposed;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 私有方法 | Private Methods
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 确保未释放
|
||||
* @en Ensure not disposed
|
||||
*/
|
||||
private _ensureNotDisposed(): void {
|
||||
if (this._disposed) {
|
||||
throw new Error('UserCodeRealm has been disposed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 用户代码隔离域服务令牌
|
||||
* @en User Code Realm service token
|
||||
*/
|
||||
export const UserCodeRealmToken = createServiceToken<UserCodeRealm>('userCodeRealm');
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Browser Platform Adapter
|
||||
* 浏览器平台适配器
|
||||
*
|
||||
* 用于独立浏览器运行时的平台适配器
|
||||
* Platform adapter for standalone browser runtime
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
IPathResolver,
|
||||
PlatformCapabilities,
|
||||
PlatformAdapterConfig
|
||||
} from '../IPlatformAdapter';
|
||||
import type { IPlatformInputSubsystem } from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* 浏览器路径解析模式
|
||||
* Browser path resolve mode
|
||||
*/
|
||||
export type BrowserPathResolveMode = 'proxy' | 'direct';
|
||||
|
||||
/**
|
||||
* 浏览器路径解析器
|
||||
* Browser path resolver
|
||||
*
|
||||
* 支持两种模式:
|
||||
* - 'proxy': 使用 /asset?path=... 格式(编辑器 "Run in Browser" 使用)
|
||||
* - 'direct': 使用直接路径如 /assets/path.png(独立 Web 构建使用)
|
||||
*
|
||||
* Supports two modes:
|
||||
* - 'proxy': Uses /asset?path=... format (for editor "Run in Browser")
|
||||
* - 'direct': Uses direct paths like /assets/path.png (for standalone web builds)
|
||||
*/
|
||||
export class BrowserPathResolver implements IPathResolver {
|
||||
private _baseUrl: string;
|
||||
private _mode: BrowserPathResolveMode;
|
||||
|
||||
constructor(baseUrl: string = '/assets', mode: BrowserPathResolveMode = 'proxy') {
|
||||
this._baseUrl = baseUrl;
|
||||
this._mode = mode;
|
||||
}
|
||||
|
||||
resolve(path: string): string {
|
||||
// 如果已经是完整 URL,直接返回
|
||||
// If already a full URL, return as-is
|
||||
if (path.startsWith('http://') ||
|
||||
path.startsWith('https://') ||
|
||||
path.startsWith('data:') ||
|
||||
path.startsWith('blob:') ||
|
||||
path.startsWith('/asset?')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (this._mode === 'proxy') {
|
||||
// Proxy mode: use /asset?path=... format
|
||||
// 代理模式:使用 /asset?path=... 格式
|
||||
return `/asset?path=${encodeURIComponent(path)}`;
|
||||
}
|
||||
|
||||
// Direct mode: use direct URL paths
|
||||
// 直接模式:使用直接 URL 路径
|
||||
|
||||
// 规范化路径:移除 ./ 前缀,统一斜杠
|
||||
// Normalize path: remove ./ prefix, unify slashes
|
||||
let normalizedPath = path.replace(/\\/g, '/');
|
||||
|
||||
// 移除开头的 ./
|
||||
if (normalizedPath.startsWith('./')) {
|
||||
normalizedPath = normalizedPath.substring(2);
|
||||
}
|
||||
|
||||
// 移除开头的 /
|
||||
if (normalizedPath.startsWith('/')) {
|
||||
normalizedPath = normalizedPath.substring(1);
|
||||
}
|
||||
|
||||
// 如果路径以 assets/ 开头,移除它(避免与 baseUrl 重复)
|
||||
// If path starts with assets/, remove it (avoid duplication with baseUrl)
|
||||
if (normalizedPath.startsWith('assets/')) {
|
||||
normalizedPath = normalizedPath.substring(7);
|
||||
}
|
||||
|
||||
// 确保 baseUrl 没有尾部斜杠
|
||||
// Ensure baseUrl has no trailing slash
|
||||
const base = this._baseUrl.replace(/\/+$/, '');
|
||||
|
||||
return `${base}/${normalizedPath}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新基础 URL
|
||||
* Update base URL
|
||||
*/
|
||||
setBaseUrl(baseUrl: string): void {
|
||||
this._baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置解析模式
|
||||
* Set resolve mode
|
||||
*/
|
||||
setMode(mode: BrowserPathResolveMode): void {
|
||||
this._mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器平台适配器配置
|
||||
* Browser platform adapter configuration
|
||||
*/
|
||||
export interface BrowserPlatformConfig {
|
||||
/** WASM 模块(预加载的)| Pre-loaded WASM module */
|
||||
wasmModule?: any;
|
||||
/** WASM 模块加载器(异步加载)| Async WASM module loader */
|
||||
wasmModuleLoader?: () => Promise<any>;
|
||||
/** 资产基础 URL | Asset base URL */
|
||||
assetBaseUrl?: string;
|
||||
/**
|
||||
* 路径解析模式 | Path resolve mode
|
||||
* - 'proxy': 使用 /asset?path=... 格式(默认,编辑器使用)
|
||||
* - 'direct': 使用直接路径(独立 Web 构建使用)
|
||||
*/
|
||||
pathResolveMode?: BrowserPathResolveMode;
|
||||
/**
|
||||
* 输入子系统工厂函数
|
||||
* Input subsystem factory function
|
||||
*/
|
||||
inputSubsystemFactory?: () => IPlatformInputSubsystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器平台适配器
|
||||
* Browser platform adapter
|
||||
*/
|
||||
export class BrowserPlatformAdapter implements IPlatformAdapter {
|
||||
readonly name = 'browser';
|
||||
|
||||
readonly capabilities: PlatformCapabilities = {
|
||||
fileSystem: false,
|
||||
hotReload: false,
|
||||
gizmos: false,
|
||||
grid: false,
|
||||
sceneEditing: false
|
||||
};
|
||||
|
||||
private _pathResolver: BrowserPathResolver;
|
||||
private _canvas: HTMLCanvasElement | null = null;
|
||||
private _config: BrowserPlatformConfig;
|
||||
private _viewportSize = { width: 0, height: 0 };
|
||||
private _inputSubsystem: IPlatformInputSubsystem | null = null;
|
||||
|
||||
constructor(config: BrowserPlatformConfig = {}) {
|
||||
this._config = config;
|
||||
this._pathResolver = new BrowserPathResolver(
|
||||
config.assetBaseUrl || '/assets',
|
||||
config.pathResolveMode || 'proxy'
|
||||
);
|
||||
}
|
||||
|
||||
get pathResolver(): IPathResolver {
|
||||
return this._pathResolver;
|
||||
}
|
||||
|
||||
async initialize(config: PlatformAdapterConfig): Promise<void> {
|
||||
// 获取 Canvas
|
||||
this._canvas = document.getElementById(config.canvasId) as HTMLCanvasElement;
|
||||
if (!this._canvas) {
|
||||
throw new Error(`Canvas not found: ${config.canvasId}`);
|
||||
}
|
||||
|
||||
// 设置尺寸
|
||||
const width = config.width || window.innerWidth;
|
||||
const height = config.height || window.innerHeight;
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
this._viewportSize = { width, height };
|
||||
|
||||
if (this._config.inputSubsystemFactory) {
|
||||
this._inputSubsystem = this._config.inputSubsystemFactory();
|
||||
}
|
||||
}
|
||||
|
||||
async getWasmModule(): Promise<any> {
|
||||
// 如果已提供模块,直接返回
|
||||
if (this._config.wasmModule) {
|
||||
return this._config.wasmModule;
|
||||
}
|
||||
|
||||
// 如果提供了加载器,使用加载器
|
||||
if (this._config.wasmModuleLoader) {
|
||||
return this._config.wasmModuleLoader();
|
||||
}
|
||||
|
||||
// 默认:尝试动态导入
|
||||
throw new Error('No WASM module or loader provided');
|
||||
}
|
||||
|
||||
getCanvas(): HTMLCanvasElement | null {
|
||||
return this._canvas;
|
||||
}
|
||||
|
||||
resize(width: number, height: number): void {
|
||||
if (this._canvas) {
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
}
|
||||
this._viewportSize = { width, height };
|
||||
}
|
||||
|
||||
getViewportSize(): { width: number; height: number } {
|
||||
return { ...this._viewportSize };
|
||||
}
|
||||
|
||||
isEditorMode(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getInputSubsystem(): IPlatformInputSubsystem | null {
|
||||
return this._inputSubsystem;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._inputSubsystem) {
|
||||
this._inputSubsystem.dispose?.();
|
||||
this._inputSubsystem = null;
|
||||
}
|
||||
this._canvas = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Editor Platform Adapter
|
||||
* 编辑器平台适配器
|
||||
*
|
||||
* 用于 Tauri 编辑器内嵌预览的平台适配器
|
||||
* Platform adapter for Tauri editor embedded preview
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
IPathResolver,
|
||||
PlatformCapabilities,
|
||||
PlatformAdapterConfig
|
||||
} from '../IPlatformAdapter';
|
||||
import type { IPlatformInputSubsystem } from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* 编辑器路径解析器
|
||||
* Editor path resolver
|
||||
*
|
||||
* 使用 Tauri 的 convertFileSrc 转换本地文件路径
|
||||
* Uses Tauri's convertFileSrc to convert local file paths
|
||||
*/
|
||||
export class EditorPathResolver implements IPathResolver {
|
||||
private _pathTransformer: (path: string) => string;
|
||||
|
||||
constructor(pathTransformer: (path: string) => string) {
|
||||
this._pathTransformer = pathTransformer;
|
||||
}
|
||||
|
||||
resolve(path: string): string {
|
||||
// 如果已经是 URL,直接返回
|
||||
if (path.startsWith('http://') ||
|
||||
path.startsWith('https://') ||
|
||||
path.startsWith('data:') ||
|
||||
path.startsWith('blob:') ||
|
||||
path.startsWith('asset://')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// 使用 Tauri 路径转换器
|
||||
return this._pathTransformer(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新路径转换器
|
||||
*/
|
||||
setPathTransformer(transformer: (path: string) => string): void {
|
||||
this._pathTransformer = transformer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器平台适配器配置
|
||||
*/
|
||||
export interface EditorPlatformConfig {
|
||||
/** WASM 模块(预加载的)*/
|
||||
wasmModule: any;
|
||||
/** 路径转换函数(使用 Tauri 的 convertFileSrc)*/
|
||||
pathTransformer: (path: string) => string;
|
||||
/** Gizmo 数据提供者 */
|
||||
gizmoDataProvider?: (component: any, entity: any, isSelected: boolean) => any;
|
||||
/** Gizmo 检查函数 */
|
||||
hasGizmoProvider?: (component: any) => boolean;
|
||||
/**
|
||||
* 输入子系统工厂函数
|
||||
* Input subsystem factory function
|
||||
*
|
||||
* 用于在 Play 模式下接收游戏输入
|
||||
* Used to receive game input in Play mode
|
||||
*/
|
||||
inputSubsystemFactory?: () => IPlatformInputSubsystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器平台适配器
|
||||
* Editor platform adapter
|
||||
*/
|
||||
export class EditorPlatformAdapter implements IPlatformAdapter {
|
||||
readonly name = 'editor';
|
||||
|
||||
readonly capabilities: PlatformCapabilities = {
|
||||
fileSystem: true,
|
||||
hotReload: true,
|
||||
gizmos: true,
|
||||
grid: true,
|
||||
sceneEditing: true
|
||||
};
|
||||
|
||||
private _pathResolver: EditorPathResolver;
|
||||
private _canvas: HTMLCanvasElement | null = null;
|
||||
private _config: EditorPlatformConfig;
|
||||
private _viewportSize = { width: 0, height: 0 };
|
||||
private _showGrid = true;
|
||||
private _showGizmos = true;
|
||||
private _inputSubsystem: IPlatformInputSubsystem | null = null;
|
||||
|
||||
constructor(config: EditorPlatformConfig) {
|
||||
this._config = config;
|
||||
this._pathResolver = new EditorPathResolver(config.pathTransformer);
|
||||
}
|
||||
|
||||
get pathResolver(): IPathResolver {
|
||||
return this._pathResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Gizmo 数据提供者
|
||||
*/
|
||||
get gizmoDataProvider() {
|
||||
return this._config.gizmoDataProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Gizmo 检查函数
|
||||
*/
|
||||
get hasGizmoProvider() {
|
||||
return this._config.hasGizmoProvider;
|
||||
}
|
||||
|
||||
async initialize(config: PlatformAdapterConfig): Promise<void> {
|
||||
// 获取 Canvas
|
||||
this._canvas = document.getElementById(config.canvasId) as HTMLCanvasElement;
|
||||
if (!this._canvas) {
|
||||
throw new Error(`Canvas not found: ${config.canvasId}`);
|
||||
}
|
||||
|
||||
// 处理 DPR 缩放
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const container = this._canvas.parentElement;
|
||||
|
||||
if (container) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const width = config.width || Math.floor(rect.width * dpr);
|
||||
const height = config.height || Math.floor(rect.height * dpr);
|
||||
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
this._canvas.style.width = `${rect.width}px`;
|
||||
this._canvas.style.height = `${rect.height}px`;
|
||||
|
||||
this._viewportSize = { width, height };
|
||||
} else {
|
||||
const width = config.width || window.innerWidth;
|
||||
const height = config.height || window.innerHeight;
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
this._viewportSize = { width, height };
|
||||
}
|
||||
|
||||
// 创建输入子系统(如果提供了工厂函数)
|
||||
// Create input subsystem (if factory provided)
|
||||
if (this._config.inputSubsystemFactory) {
|
||||
this._inputSubsystem = this._config.inputSubsystemFactory();
|
||||
}
|
||||
}
|
||||
|
||||
async getWasmModule(): Promise<any> {
|
||||
return this._config.wasmModule;
|
||||
}
|
||||
|
||||
getCanvas(): HTMLCanvasElement | null {
|
||||
return this._canvas;
|
||||
}
|
||||
|
||||
resize(width: number, height: number): void {
|
||||
if (this._canvas) {
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
}
|
||||
this._viewportSize = { width, height };
|
||||
}
|
||||
|
||||
getViewportSize(): { width: number; height: number } {
|
||||
return { ...this._viewportSize };
|
||||
}
|
||||
|
||||
isEditorMode(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
setShowGrid(show: boolean): void {
|
||||
this._showGrid = show;
|
||||
}
|
||||
|
||||
getShowGrid(): boolean {
|
||||
return this._showGrid;
|
||||
}
|
||||
|
||||
setShowGizmos(show: boolean): void {
|
||||
this._showGizmos = show;
|
||||
}
|
||||
|
||||
getShowGizmos(): boolean {
|
||||
return this._showGizmos;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入子系统
|
||||
* Get input subsystem
|
||||
*
|
||||
* 用于 InputSystem 接收游戏输入事件
|
||||
* Used by InputSystem to receive game input events
|
||||
*/
|
||||
getInputSubsystem(): IPlatformInputSubsystem | null {
|
||||
return this._inputSubsystem;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._inputSubsystem) {
|
||||
this._inputSubsystem.dispose?.();
|
||||
this._inputSubsystem = null;
|
||||
}
|
||||
this._canvas = null;
|
||||
}
|
||||
}
|
||||
12
packages/engine/runtime-core/src/adapters/index.ts
Normal file
12
packages/engine/runtime-core/src/adapters/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Platform Adapters
|
||||
* 平台适配器
|
||||
*/
|
||||
|
||||
export {
|
||||
BrowserPlatformAdapter,
|
||||
BrowserPathResolver,
|
||||
type BrowserPlatformConfig,
|
||||
type BrowserPathResolveMode
|
||||
} from './BrowserPlatformAdapter';
|
||||
export { EditorPlatformAdapter, EditorPathResolver, type EditorPlatformConfig } from './EditorPlatformAdapter';
|
||||
204
packages/engine/runtime-core/src/index.ts
Normal file
204
packages/engine/runtime-core/src/index.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
export {
|
||||
RuntimePluginManager,
|
||||
runtimePluginManager,
|
||||
type SystemContext,
|
||||
type ModuleManifest,
|
||||
type IRuntimeModule,
|
||||
type IRuntimePlugin
|
||||
} from './PluginManager';
|
||||
|
||||
// Plugin Lifecycle State
|
||||
export {
|
||||
PluginLifecycleState,
|
||||
VALID_STATE_TRANSITIONS,
|
||||
isValidStateTransition,
|
||||
isPluginOperable,
|
||||
isPluginLoading,
|
||||
isPluginAvailable,
|
||||
type PluginState,
|
||||
type PluginStateChangeEvent,
|
||||
type PluginStateChangeListener
|
||||
} from './PluginState';
|
||||
|
||||
export {
|
||||
createPlugin,
|
||||
registerPlugin,
|
||||
initializeRuntime,
|
||||
createSystemsForScene,
|
||||
resetRuntime,
|
||||
type RuntimeConfig
|
||||
} from './RuntimeBootstrap';
|
||||
|
||||
// Plugin Loader
|
||||
export {
|
||||
PluginLoader,
|
||||
loadPlugin,
|
||||
loadEnabledPlugins,
|
||||
registerStaticPlugin,
|
||||
getLoadedPlugins,
|
||||
resetPluginLoader,
|
||||
type PluginLoadState,
|
||||
type PluginSourceType,
|
||||
type PluginPackageInfo,
|
||||
type PluginConfig,
|
||||
type ProjectPluginConfig,
|
||||
type PluginLoadConfig,
|
||||
type PluginLoadInfo,
|
||||
type PluginLoaderConfig
|
||||
} from './PluginLoader';
|
||||
|
||||
export {
|
||||
BUILTIN_PLUGIN_PACKAGES,
|
||||
createDefaultProjectConfig,
|
||||
mergeProjectConfig,
|
||||
createProjectConfigFromEnabledList,
|
||||
convertToPluginLoadConfigs,
|
||||
type ProjectConfig,
|
||||
type ExtendedPluginPackageInfo
|
||||
} from './ProjectConfig';
|
||||
|
||||
// Platform Adapter
|
||||
export {
|
||||
DefaultPathResolver,
|
||||
type IPlatformAdapter,
|
||||
type IPathResolver,
|
||||
type PlatformCapabilities,
|
||||
type PlatformAdapterConfig
|
||||
} from './IPlatformAdapter';
|
||||
|
||||
// Game Runtime
|
||||
export {
|
||||
GameRuntime,
|
||||
createGameRuntime,
|
||||
type GameRuntimeConfig,
|
||||
type RuntimeState
|
||||
} from './GameRuntime';
|
||||
|
||||
// Runtime Mode
|
||||
export {
|
||||
RuntimeMode,
|
||||
getRuntimeModeConfig,
|
||||
isEditorMode,
|
||||
shouldEnableGameLogic,
|
||||
type RuntimeModeConfig
|
||||
} from './RuntimeMode';
|
||||
|
||||
// User Code Realm
|
||||
export {
|
||||
UserCodeRealm,
|
||||
UserCodeRealmToken,
|
||||
type UserCodeRealmConfig,
|
||||
type UserSystemInfo,
|
||||
type UserComponentInfo
|
||||
} from './UserCodeRealm';
|
||||
|
||||
// ImportMap Generator
|
||||
export {
|
||||
generateImportMap,
|
||||
generateImportMapEntries,
|
||||
generateImportMapScript,
|
||||
extractModuleId,
|
||||
getPackageName,
|
||||
collectExternalDependencies,
|
||||
sortModulesByDependencies,
|
||||
type ImportMapMode,
|
||||
type ImportMapConfig,
|
||||
type ImportMapEntry
|
||||
} from './ImportMapGenerator';
|
||||
|
||||
// Platform Adapters
|
||||
export {
|
||||
BrowserPlatformAdapter,
|
||||
BrowserPathResolver,
|
||||
type BrowserPlatformConfig,
|
||||
type BrowserPathResolveMode,
|
||||
EditorPlatformAdapter,
|
||||
EditorPathResolver,
|
||||
type EditorPlatformConfig
|
||||
} from './adapters';
|
||||
|
||||
// Browser File System Service
|
||||
export {
|
||||
BrowserFileSystemService,
|
||||
createBrowserFileSystem,
|
||||
type BrowserFileSystemOptions
|
||||
} from './services/BrowserFileSystemService';
|
||||
|
||||
// Runtime Scene Manager
|
||||
export {
|
||||
RuntimeSceneManager,
|
||||
RuntimeSceneManagerToken,
|
||||
type IRuntimeSceneManager,
|
||||
type SceneInfo,
|
||||
type SceneLoadOptions,
|
||||
type SceneLoader
|
||||
} from './services/RuntimeSceneManager';
|
||||
|
||||
// ============================================================================
|
||||
// 便捷 Re-exports | Convenience Re-exports
|
||||
// ============================================================================
|
||||
// 以下是常用类型的便捷 re-export,让运行时消费者无需添加额外依赖
|
||||
// These are convenience re-exports for common types, so runtime consumers
|
||||
// don't need to add extra dependencies
|
||||
|
||||
// 输入系统(运行时常用)| Input System (commonly used in runtime)
|
||||
export {
|
||||
Input,
|
||||
InputManager,
|
||||
InputSystem,
|
||||
MouseButton,
|
||||
type InputSystemConfig,
|
||||
type KeyState,
|
||||
type MouseButtonState,
|
||||
type KeyboardEventInfo,
|
||||
type MouseEventInfo,
|
||||
type WheelEventInfo,
|
||||
type TouchInfo,
|
||||
type TouchEvent
|
||||
} from '@esengine/engine-core';
|
||||
|
||||
// 向量接口(运行时常用)| Vector interfaces (commonly used in runtime)
|
||||
export type { IVector2, IVector3 } from '@esengine/ecs-framework-math';
|
||||
|
||||
// 服务注册基础设施(创建和使用 Token 必需)
|
||||
// Service registry infrastructure (required for creating and using tokens)
|
||||
export {
|
||||
PluginServiceRegistry,
|
||||
createServiceToken,
|
||||
type ServiceToken
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
// ============================================================================
|
||||
// 注意:服务 Token 应从其定义模块导入
|
||||
// Note: Service tokens should be imported from their defining modules
|
||||
// ============================================================================
|
||||
// 以下 Token 已移除,请直接从源模块导入:
|
||||
// The following tokens have been removed, import from source:
|
||||
//
|
||||
// - TransformTypeToken -> @esengine/engine-core
|
||||
// - RenderSystemToken -> @esengine/ecs-engine-bindgen
|
||||
// - EngineIntegrationToken -> @esengine/ecs-engine-bindgen
|
||||
// - TextureServiceToken -> @esengine/ecs-engine-bindgen
|
||||
// - AssetManagerToken -> @esengine/asset-system
|
||||
//
|
||||
// 这遵循 "谁定义接口,谁导出 Token" 原则
|
||||
// This follows the "whoever defines the interface, exports the token" principle
|
||||
|
||||
// Dependency Utils
|
||||
export {
|
||||
// 类型 | Types
|
||||
type IDependable,
|
||||
type TopologicalSortOptions,
|
||||
type TopologicalSortResult,
|
||||
type DependencyValidationResult,
|
||||
// 依赖 ID 解析 | Dependency ID Resolution
|
||||
resolveDependencyId,
|
||||
extractShortId,
|
||||
getPackageName as getPackageNameFromId,
|
||||
// 拓扑排序 | Topological Sort
|
||||
topologicalSort,
|
||||
// 依赖验证 | Dependency Validation
|
||||
validateDependencies as validateItemDependencies,
|
||||
getAllDependencies,
|
||||
getReverseDependencies
|
||||
} from './utils/DependencyUtils';
|
||||
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* Browser File System Service
|
||||
* 浏览器文件系统服务
|
||||
*
|
||||
* 在浏览器运行时环境中,通过 HTTP fetch 加载资产文件。
|
||||
* 使用资产目录(asset-catalog.json)来解析 GUID 到实际 URL。
|
||||
*
|
||||
* In browser runtime environment, loads asset files via HTTP fetch.
|
||||
* Uses asset catalog to resolve GUIDs to actual URLs.
|
||||
*/
|
||||
|
||||
import type {
|
||||
IAssetCatalog,
|
||||
IAssetCatalogEntry,
|
||||
IAssetBundleInfo,
|
||||
AssetLoadStrategy
|
||||
} from '@esengine/asset-system';
|
||||
|
||||
/**
|
||||
* Browser file system service options
|
||||
* 浏览器文件系统服务配置
|
||||
*/
|
||||
export interface BrowserFileSystemOptions {
|
||||
/** Base URL for assets (e.g., '/assets' or 'https://cdn.example.com/assets') */
|
||||
baseUrl?: string;
|
||||
/** Asset catalog URL */
|
||||
catalogUrl?: string;
|
||||
/** Enable caching */
|
||||
enableCache?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser File System Service
|
||||
* 浏览器文件系统服务
|
||||
*
|
||||
* Provides file system-like API for browser environments
|
||||
* by fetching files over HTTP. Supports both file-based and bundle-based loading.
|
||||
* 为浏览器环境提供类文件系统 API,通过 HTTP fetch 加载文件。
|
||||
* 支持基于文件和基于包的两种加载模式。
|
||||
*/
|
||||
export class BrowserFileSystemService {
|
||||
private _baseUrl: string;
|
||||
private _catalogUrl: string;
|
||||
private _catalog: IAssetCatalog | null = null;
|
||||
private _cache = new Map<string, string>();
|
||||
private _bundleCache = new Map<string, ArrayBuffer>();
|
||||
private _enableCache: boolean;
|
||||
private _initialized = false;
|
||||
|
||||
constructor(options: BrowserFileSystemOptions = {}) {
|
||||
this._baseUrl = options.baseUrl ?? '/assets';
|
||||
this._catalogUrl = options.catalogUrl ?? '/asset-catalog.json';
|
||||
this._enableCache = options.enableCache ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize service and load catalog
|
||||
* 初始化服务并加载目录
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this._initialized) return;
|
||||
|
||||
try {
|
||||
await this._loadCatalog();
|
||||
this._initialized = true;
|
||||
|
||||
const strategy = this._catalog?.loadStrategy ?? 'file';
|
||||
const assetCount = Object.keys(this._catalog?.entries ?? {}).length;
|
||||
console.log(`[BrowserFileSystem] Initialized: ${assetCount} assets, strategy=${strategy}`);
|
||||
} catch (error) {
|
||||
console.warn('[BrowserFileSystem] Failed to load catalog:', error);
|
||||
// Continue without catalog - will use path-based loading
|
||||
// 无目录时继续,使用基于路径的加载
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset catalog
|
||||
* 加载资产目录
|
||||
*/
|
||||
private async _loadCatalog(): Promise<void> {
|
||||
const response = await fetch(this._catalogUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch catalog: ${response.status}`);
|
||||
}
|
||||
|
||||
const rawCatalog = await response.json();
|
||||
|
||||
// Normalize catalog format (handle legacy format without loadStrategy)
|
||||
// 规范化目录格式(处理没有 loadStrategy 的旧格式)
|
||||
this._catalog = this._normalizeCatalog(rawCatalog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize catalog to ensure it has all required fields
|
||||
* 规范化目录,确保包含所有必需字段
|
||||
*
|
||||
* Supports both 'entries' (IAssetCatalog) and 'assets' (IRuntimeCatalog) field names.
|
||||
* 同时支持 'entries' (IAssetCatalog) 和 'assets' (IRuntimeCatalog) 字段名。
|
||||
*/
|
||||
private _normalizeCatalog(raw: Record<string, unknown>): IAssetCatalog {
|
||||
// Determine load strategy
|
||||
// 确定加载策略
|
||||
let loadStrategy: AssetLoadStrategy = 'file';
|
||||
// Only use bundle strategy if explicitly set or bundles is non-empty
|
||||
// 仅当明确设置或 bundles 非空时才使用 bundle 策略
|
||||
const hasBundles = raw.bundles && typeof raw.bundles === 'object' && Object.keys(raw.bundles as object).length > 0;
|
||||
if (raw.loadStrategy === 'bundle' || hasBundles) {
|
||||
loadStrategy = 'bundle';
|
||||
}
|
||||
|
||||
// Support both 'entries' and 'assets' field names for compatibility
|
||||
// 同时支持 'entries' 和 'assets' 字段名以保持兼容性
|
||||
const entries = (raw.entries ?? raw.assets) as Record<string, IAssetCatalogEntry> ?? {};
|
||||
|
||||
return {
|
||||
version: (raw.version as string) ?? '1.0.0',
|
||||
createdAt: (raw.createdAt as number) ?? Date.now(),
|
||||
loadStrategy,
|
||||
entries,
|
||||
bundles: (raw.bundles as Record<string, IAssetBundleInfo>) ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current load strategy
|
||||
* 获取当前加载策略
|
||||
*/
|
||||
get loadStrategy(): AssetLoadStrategy {
|
||||
return this._catalog?.loadStrategy ?? 'file';
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file content as string
|
||||
* @param path - Can be GUID, relative path, or absolute path
|
||||
*/
|
||||
async readFile(path: string): Promise<string> {
|
||||
const url = this._resolveUrl(path);
|
||||
|
||||
// Check cache
|
||||
if (this._enableCache && this._cache.has(url)) {
|
||||
return this._cache.get(url)!;
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch file: ${url} (${response.status})`);
|
||||
}
|
||||
|
||||
const content = await response.text();
|
||||
|
||||
// Cache result
|
||||
if (this._enableCache) {
|
||||
this._cache.set(url, content);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file as binary (ArrayBuffer)
|
||||
*/
|
||||
async readBinary(path: string): Promise<ArrayBuffer> {
|
||||
const url = this._resolveUrl(path);
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch binary: ${url} (${response.status})`);
|
||||
}
|
||||
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file as Blob
|
||||
*/
|
||||
async readBlob(path: string): Promise<Blob> {
|
||||
const url = this._resolveUrl(path);
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch blob: ${url} (${response.status})`);
|
||||
}
|
||||
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file exists (via HEAD request)
|
||||
*/
|
||||
async exists(path: string): Promise<boolean> {
|
||||
const url = this._resolveUrl(path);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { method: 'HEAD' });
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path to URL
|
||||
* Handles GUID, relative path, and absolute path
|
||||
*/
|
||||
private _resolveUrl(path: string): string {
|
||||
// Check if it's a GUID and we have a catalog
|
||||
if (this._catalog && this._isGuid(path)) {
|
||||
const entry = this._catalog.entries[path];
|
||||
if (entry) {
|
||||
return this._pathToUrl(entry.path);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's an absolute Windows path (e.g., F:\...)
|
||||
if (/^[A-Za-z]:[\\/]/.test(path)) {
|
||||
// Try to extract relative path from absolute path
|
||||
const relativePath = this._extractRelativePath(path);
|
||||
if (relativePath) {
|
||||
return this._pathToUrl(relativePath);
|
||||
}
|
||||
// Fallback: use just the filename
|
||||
const filename = path.split(/[\\/]/).pop();
|
||||
return `${this._baseUrl}/${filename}`;
|
||||
}
|
||||
|
||||
// Check if it's already a URL
|
||||
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('/')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Treat as relative path
|
||||
return this._pathToUrl(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relative path to URL
|
||||
*/
|
||||
private _pathToUrl(relativePath: string): string {
|
||||
// Normalize path separators
|
||||
const normalized = relativePath.replace(/\\/g, '/');
|
||||
|
||||
// Remove leading 'assets/' if baseUrl already includes it
|
||||
let cleanPath = normalized;
|
||||
if (cleanPath.startsWith('assets/') && this._baseUrl.endsWith('/assets')) {
|
||||
cleanPath = cleanPath.substring(7);
|
||||
}
|
||||
|
||||
// Ensure no double slashes
|
||||
const base = this._baseUrl.endsWith('/') ? this._baseUrl.slice(0, -1) : this._baseUrl;
|
||||
const path = cleanPath.startsWith('/') ? cleanPath.slice(1) : cleanPath;
|
||||
|
||||
return `${base}/${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract relative path from absolute path
|
||||
*/
|
||||
private _extractRelativePath(absolutePath: string): string | null {
|
||||
const normalized = absolutePath.replace(/\\/g, '/');
|
||||
|
||||
// Look for 'assets/' in the path
|
||||
const assetsIndex = normalized.toLowerCase().indexOf('/assets/');
|
||||
if (assetsIndex >= 0) {
|
||||
return normalized.substring(assetsIndex + 1); // Include 'assets/'
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if string looks like a GUID
|
||||
*/
|
||||
private _isGuid(str: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset metadata from catalog
|
||||
* 从目录获取资产元数据
|
||||
*/
|
||||
getAssetMetadata(guidOrPath: string): IAssetCatalogEntry | null {
|
||||
if (!this._catalog) return null;
|
||||
|
||||
// Try as GUID
|
||||
if (this._catalog.entries[guidOrPath]) {
|
||||
return this._catalog.entries[guidOrPath];
|
||||
}
|
||||
|
||||
// Try as path
|
||||
for (const entry of Object.values(this._catalog.entries)) {
|
||||
if (entry.path === guidOrPath) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all assets of a specific type
|
||||
* 获取指定类型的所有资产
|
||||
*/
|
||||
getAssetsByType(type: string): IAssetCatalogEntry[] {
|
||||
if (!this._catalog) return [];
|
||||
|
||||
return Object.values(this._catalog.entries)
|
||||
.filter(entry => entry.type === type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
* 清除缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
this._cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get catalog
|
||||
* 获取目录
|
||||
*/
|
||||
get catalog(): IAssetCatalog | null {
|
||||
return this._catalog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if initialized
|
||||
*/
|
||||
get isInitialized(): boolean {
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose service and clear resources
|
||||
* Required by IService interface
|
||||
*/
|
||||
dispose(): void {
|
||||
this._cache.clear();
|
||||
this._catalog = null;
|
||||
this._initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and register browser file system service
|
||||
*/
|
||||
export function createBrowserFileSystem(options?: BrowserFileSystemOptions): BrowserFileSystemService {
|
||||
return new BrowserFileSystemService(options);
|
||||
}
|
||||
449
packages/engine/runtime-core/src/services/RuntimeSceneManager.ts
Normal file
449
packages/engine/runtime-core/src/services/RuntimeSceneManager.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* 运行时场景管理器
|
||||
* Runtime Scene Manager
|
||||
*
|
||||
* 提供场景加载和切换 API,供用户脚本使用。
|
||||
* Provides scene loading and transition API for user scripts.
|
||||
*
|
||||
* 生命周期设计 | Lifecycle Design:
|
||||
* - reset(): 清理会话状态,保留核心功能(用于 Play/Stop 切换)
|
||||
* - dispose(): 完全销毁,释放所有资源(用于编辑器关闭)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在用户脚本中获取场景管理器
|
||||
* // Get scene manager in user script
|
||||
* const sceneManager = services.get(RuntimeSceneManagerToken);
|
||||
*
|
||||
* // 加载场景(按名称)
|
||||
* // Load scene by name
|
||||
* await sceneManager.loadScene('GameScene');
|
||||
*
|
||||
* // 加载场景(按路径)
|
||||
* // Load scene by path
|
||||
* await sceneManager.loadSceneByPath('./scenes/Level1.ecs');
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 场景信息
|
||||
* Scene info
|
||||
*/
|
||||
export interface SceneInfo {
|
||||
/** 场景名称 | Scene name */
|
||||
name: string;
|
||||
/** 场景路径(相对于构建输出目录)| Scene path (relative to build output) */
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景加载选项
|
||||
* Scene load options
|
||||
*/
|
||||
export interface SceneLoadOptions {
|
||||
/**
|
||||
* 是否显示加载界面
|
||||
* Whether to show loading screen
|
||||
*/
|
||||
showLoading?: boolean;
|
||||
|
||||
/**
|
||||
* 过渡效果类型
|
||||
* Transition effect type
|
||||
*/
|
||||
transition?: 'none' | 'fade' | 'slide';
|
||||
|
||||
/**
|
||||
* 过渡持续时间(毫秒)
|
||||
* Transition duration in milliseconds
|
||||
*/
|
||||
transitionDuration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景加载器函数类型
|
||||
* Scene loader function type
|
||||
*/
|
||||
export type SceneLoader = (url: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 运行时场景管理器接口
|
||||
* Runtime Scene Manager Interface
|
||||
*
|
||||
* 生命周期方法 | Lifecycle Methods:
|
||||
* - reset(): 重置会话状态(Play/Stop 切换时调用)
|
||||
* - dispose(): 完全销毁(编辑器关闭时调用)
|
||||
*/
|
||||
export interface IRuntimeSceneManager {
|
||||
/**
|
||||
* 获取当前场景名称
|
||||
* Get current scene name
|
||||
*/
|
||||
readonly currentSceneName: string | null;
|
||||
|
||||
/**
|
||||
* 获取可用场景列表
|
||||
* Get available scene list
|
||||
*/
|
||||
readonly availableScenes: readonly SceneInfo[];
|
||||
|
||||
/**
|
||||
* 是否正在加载场景
|
||||
* Whether a scene is currently loading
|
||||
*/
|
||||
readonly isLoading: boolean;
|
||||
|
||||
/**
|
||||
* 注册可用场景
|
||||
* Register available scenes
|
||||
*/
|
||||
registerScenes(scenes: SceneInfo[]): void;
|
||||
|
||||
/**
|
||||
* 按名称加载场景
|
||||
* Load scene by name
|
||||
*/
|
||||
loadScene(sceneName: string, options?: SceneLoadOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* 按路径加载场景
|
||||
* Load scene by path
|
||||
*/
|
||||
loadSceneByPath(path: string, options?: SceneLoadOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* 重新加载当前场景
|
||||
* Reload current scene
|
||||
*/
|
||||
reloadCurrentScene(options?: SceneLoadOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* 添加场景加载开始监听器
|
||||
* Add scene load start listener
|
||||
*/
|
||||
onLoadStart(callback: (sceneName: string) => void): () => void;
|
||||
|
||||
/**
|
||||
* 添加场景加载完成监听器
|
||||
* Add scene load complete listener
|
||||
*/
|
||||
onLoadComplete(callback: (sceneName: string) => void): () => void;
|
||||
|
||||
/**
|
||||
* 添加场景加载错误监听器
|
||||
* Add scene load error listener
|
||||
*/
|
||||
onLoadError(callback: (error: Error, sceneName: string) => void): () => void;
|
||||
|
||||
/**
|
||||
* 设置场景加载器
|
||||
* Set scene loader
|
||||
*
|
||||
* 用于更新场景加载函数(如 Play 模式切换时)。
|
||||
* Used to update scene loader function (e.g., during Play mode transitions).
|
||||
*/
|
||||
setSceneLoader(loader: SceneLoader): void;
|
||||
|
||||
/**
|
||||
* 设置基础 URL
|
||||
* Set base URL
|
||||
*/
|
||||
setBaseUrl(baseUrl: string): void;
|
||||
|
||||
/**
|
||||
* 重置会话状态
|
||||
* Reset session state
|
||||
*
|
||||
* 清理监听器和当前场景状态,但保留 sceneLoader。
|
||||
* 用于 Play/Stop 切换时调用。
|
||||
*
|
||||
* Clears listeners and current scene state, but keeps sceneLoader.
|
||||
* Called during Play/Stop transitions.
|
||||
*/
|
||||
reset(): void;
|
||||
|
||||
/**
|
||||
* 完全释放资源
|
||||
* Dispose all resources
|
||||
*
|
||||
* 销毁实例,清理所有资源。
|
||||
* 仅在编辑器关闭时调用。
|
||||
*
|
||||
* Destroys the instance, cleans up all resources.
|
||||
* Only called when editor closes.
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时场景管理器服务令牌
|
||||
* Runtime Scene Manager Service Token
|
||||
*/
|
||||
export const RuntimeSceneManagerToken = createServiceToken<IRuntimeSceneManager>('runtimeSceneManager');
|
||||
|
||||
/**
|
||||
* 运行时场景管理器实现
|
||||
* Runtime Scene Manager Implementation
|
||||
*
|
||||
* 实现 IService 接口以兼容 ServiceContainer。
|
||||
* Implements IService for ServiceContainer compatibility.
|
||||
*/
|
||||
export class RuntimeSceneManager implements IRuntimeSceneManager {
|
||||
private _scenes = new Map<string, SceneInfo>();
|
||||
private _currentSceneName: string | null = null;
|
||||
private _currentScenePath: string | null = null;
|
||||
private _isLoading = false;
|
||||
private _sceneLoader: SceneLoader | null = null;
|
||||
private _baseUrl: string;
|
||||
private _disposed = false;
|
||||
|
||||
// 事件监听器 | Event listeners
|
||||
private _loadStartListeners = new Set<(sceneName: string) => void>();
|
||||
private _loadCompleteListeners = new Set<(sceneName: string) => void>();
|
||||
private _loadErrorListeners = new Set<(error: Error, sceneName: string) => void>();
|
||||
|
||||
/**
|
||||
* 创建运行时场景管理器
|
||||
* Create runtime scene manager
|
||||
*
|
||||
* @param sceneLoader 场景加载函数 | Scene loader function
|
||||
* @param baseUrl 场景文件基础 URL | Scene files base URL
|
||||
*/
|
||||
constructor(sceneLoader: SceneLoader, baseUrl: string = './scenes') {
|
||||
this._sceneLoader = sceneLoader;
|
||||
this._baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
get currentSceneName(): string | null {
|
||||
return this._currentSceneName;
|
||||
}
|
||||
|
||||
get availableScenes(): readonly SceneInfo[] {
|
||||
return Array.from(this._scenes.values());
|
||||
}
|
||||
|
||||
get isLoading(): boolean {
|
||||
return this._isLoading;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置场景加载器
|
||||
* Set scene loader
|
||||
*/
|
||||
setSceneLoader(loader: SceneLoader): void {
|
||||
this._sceneLoader = loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置基础 URL
|
||||
* Set base URL
|
||||
*/
|
||||
setBaseUrl(baseUrl: string): void {
|
||||
this._baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
registerScenes(scenes: SceneInfo[]): void {
|
||||
for (const scene of scenes) {
|
||||
this._scenes.set(scene.name, scene);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从目录或配置自动发现场景
|
||||
* Auto-discover scenes from catalog or config
|
||||
*/
|
||||
registerScenesFromCatalog(
|
||||
catalog: { scenes?: Array<{ name: string; path: string }> }
|
||||
): void {
|
||||
if (catalog.scenes) {
|
||||
this.registerScenes(catalog.scenes);
|
||||
}
|
||||
}
|
||||
|
||||
async loadScene(sceneName: string, options?: SceneLoadOptions): Promise<void> {
|
||||
const sceneInfo = this._scenes.get(sceneName);
|
||||
if (!sceneInfo) {
|
||||
// 尝试使用场景名作为路径
|
||||
// Try using scene name as path
|
||||
const guessedPath = `${this._baseUrl}/${sceneName}.ecs`;
|
||||
return this.loadSceneByPath(guessedPath, options);
|
||||
}
|
||||
|
||||
return this.loadSceneByPath(sceneInfo.path, options);
|
||||
}
|
||||
|
||||
async loadSceneByPath(path: string, options?: SceneLoadOptions): Promise<void> {
|
||||
if (!this._sceneLoader) {
|
||||
throw new Error('[RuntimeSceneManager] Scene loader not set');
|
||||
}
|
||||
|
||||
if (this._isLoading) {
|
||||
console.warn('[RuntimeSceneManager] Scene is already loading, ignoring request');
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建完整 URL | Build full URL
|
||||
// Check if path is already absolute (http, relative ./, Unix /, or Windows drive letter)
|
||||
// 检查路径是否已经是绝对路径(http、相对 ./、Unix /、或 Windows 驱动器号)
|
||||
let fullPath = path;
|
||||
const isAbsolutePath = path.startsWith('http') ||
|
||||
path.startsWith('./') ||
|
||||
path.startsWith('/') ||
|
||||
(path.length > 1 && path[1] === ':'); // Windows absolute path like C:\ or F:\
|
||||
|
||||
if (!isAbsolutePath) {
|
||||
fullPath = `${this._baseUrl}/${path}`;
|
||||
}
|
||||
|
||||
// 提取场景名称 | Extract scene name
|
||||
const sceneName = this._extractSceneName(path);
|
||||
|
||||
this._isLoading = true;
|
||||
this._notifyLoadStart(sceneName);
|
||||
|
||||
try {
|
||||
// TODO: 实现过渡效果 | TODO: Implement transition effects
|
||||
// if (options?.transition && options.transition !== 'none') {
|
||||
// await this._startTransition(options.transition, options.transitionDuration);
|
||||
// }
|
||||
|
||||
await this._sceneLoader(fullPath);
|
||||
|
||||
this._currentSceneName = sceneName;
|
||||
this._currentScenePath = fullPath;
|
||||
this._isLoading = false;
|
||||
|
||||
this._notifyLoadComplete(sceneName);
|
||||
|
||||
console.log(`[RuntimeSceneManager] Scene loaded: ${sceneName}`);
|
||||
} catch (error) {
|
||||
this._isLoading = false;
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this._notifyLoadError(err, sceneName);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async reloadCurrentScene(options?: SceneLoadOptions): Promise<void> {
|
||||
if (!this._currentScenePath) {
|
||||
throw new Error('[RuntimeSceneManager] No current scene to reload');
|
||||
}
|
||||
|
||||
return this.loadSceneByPath(this._currentScenePath, options);
|
||||
}
|
||||
|
||||
onLoadStart(callback: (sceneName: string) => void): () => void {
|
||||
this._loadStartListeners.add(callback);
|
||||
return () => this._loadStartListeners.delete(callback);
|
||||
}
|
||||
|
||||
onLoadComplete(callback: (sceneName: string) => void): () => void {
|
||||
this._loadCompleteListeners.add(callback);
|
||||
return () => this._loadCompleteListeners.delete(callback);
|
||||
}
|
||||
|
||||
onLoadError(callback: (error: Error, sceneName: string) => void): () => void {
|
||||
this._loadErrorListeners.add(callback);
|
||||
return () => this._loadErrorListeners.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查场景是否已注册
|
||||
* Check if scene is registered
|
||||
*/
|
||||
hasScene(sceneName: string): boolean {
|
||||
return this._scenes.has(sceneName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取场景路径
|
||||
* Get scene path
|
||||
*/
|
||||
getScenePath(sceneName: string): string | null {
|
||||
return this._scenes.get(sceneName)?.path ?? null;
|
||||
}
|
||||
|
||||
// ==================== 私有方法 | Private Methods ====================
|
||||
|
||||
private _extractSceneName(path: string): string {
|
||||
// 从路径中提取场景名称 | Extract scene name from path
|
||||
// ./scenes/Level1.ecs -> Level1
|
||||
// scenes/GameScene.ecs -> GameScene
|
||||
const fileName = path.split('/').pop() || path;
|
||||
return fileName.replace(/\.ecs$/, '');
|
||||
}
|
||||
|
||||
private _notifyLoadStart(sceneName: string): void {
|
||||
for (const listener of this._loadStartListeners) {
|
||||
try {
|
||||
listener(sceneName);
|
||||
} catch (e) {
|
||||
console.error('[RuntimeSceneManager] Error in load start listener:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _notifyLoadComplete(sceneName: string): void {
|
||||
for (const listener of this._loadCompleteListeners) {
|
||||
try {
|
||||
listener(sceneName);
|
||||
} catch (e) {
|
||||
console.error('[RuntimeSceneManager] Error in load complete listener:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _notifyLoadError(error: Error, sceneName: string): void {
|
||||
for (const listener of this._loadErrorListeners) {
|
||||
try {
|
||||
listener(error, sceneName);
|
||||
} catch (e) {
|
||||
console.error('[RuntimeSceneManager] Error in load error listener:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 生命周期方法 | Lifecycle Methods ====================
|
||||
|
||||
/**
|
||||
* 重置会话状态
|
||||
* Reset session state
|
||||
*
|
||||
* 清理监听器和当前场景状态,但保留 sceneLoader 和 baseUrl。
|
||||
* 用于 Play/Stop 切换时调用,允许实例复用。
|
||||
*
|
||||
* Clears listeners and current scene state, but keeps sceneLoader and baseUrl.
|
||||
* Called during Play/Stop transitions, allows instance reuse.
|
||||
*/
|
||||
reset(): void {
|
||||
this._loadStartListeners.clear();
|
||||
this._loadCompleteListeners.clear();
|
||||
this._loadErrorListeners.clear();
|
||||
this._scenes.clear();
|
||||
this._currentSceneName = null;
|
||||
this._currentScenePath = null;
|
||||
this._isLoading = false;
|
||||
// 注意:保留 _sceneLoader 和 _baseUrl
|
||||
// Note: Keep _sceneLoader and _baseUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* 完全释放资源
|
||||
* Dispose all resources
|
||||
*
|
||||
* 销毁实例,清理所有资源包括 sceneLoader。
|
||||
* 仅在编辑器完全关闭时调用。
|
||||
*
|
||||
* Destroys the instance, cleans up all resources including sceneLoader.
|
||||
* Only called when editor completely closes.
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this._disposed) return;
|
||||
|
||||
this.reset();
|
||||
this._sceneLoader = null;
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
472
packages/engine/runtime-core/src/utils/DependencyUtils.ts
Normal file
472
packages/engine/runtime-core/src/utils/DependencyUtils.ts
Normal file
@@ -0,0 +1,472 @@
|
||||
/**
|
||||
* @zh 依赖管理工具
|
||||
* @en Dependency Management Utilities
|
||||
*
|
||||
* @zh 提供统一的依赖解析、拓扑排序和验证功能
|
||||
* @en Provides unified dependency resolution, topological sorting, and validation
|
||||
*
|
||||
* @zh 设计原则 | Design principles:
|
||||
* 1. 单一实现 - 所有依赖处理逻辑集中在这里
|
||||
* 2. 泛型设计 - 支持任何带有 id 和 dependencies 的对象
|
||||
* 3. 算法可选 - 支持 DFS 和 Kahn 两种排序算法
|
||||
*/
|
||||
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('DependencyUtils');
|
||||
|
||||
// ============================================================================
|
||||
// 类型定义 | Type Definitions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 可排序的依赖项接口
|
||||
* @en Interface for sortable dependency items
|
||||
*/
|
||||
export interface IDependable {
|
||||
/**
|
||||
* @zh 唯一标识符
|
||||
* @en Unique identifier
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* @zh 依赖项 ID 列表
|
||||
* @en List of dependency IDs
|
||||
*/
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 拓扑排序选项
|
||||
* @en Topological sort options
|
||||
*/
|
||||
export interface TopologicalSortOptions {
|
||||
/**
|
||||
* @zh 排序算法
|
||||
* @en Sorting algorithm
|
||||
* @default 'kahn'
|
||||
*/
|
||||
algorithm?: 'dfs' | 'kahn';
|
||||
|
||||
/**
|
||||
* @zh 是否检测循环依赖
|
||||
* @en Whether to detect circular dependencies
|
||||
* @default true
|
||||
*/
|
||||
detectCycles?: boolean;
|
||||
|
||||
/**
|
||||
* @zh ID 解析函数(将短 ID 转换为完整 ID)
|
||||
* @en ID resolver function (convert short ID to full ID)
|
||||
*/
|
||||
resolveId?: (id: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 拓扑排序结果
|
||||
* @en Topological sort result
|
||||
*/
|
||||
export interface TopologicalSortResult<T> {
|
||||
/**
|
||||
* @zh 排序后的项目列表
|
||||
* @en Sorted items list
|
||||
*/
|
||||
sorted: T[];
|
||||
|
||||
/**
|
||||
* @zh 是否存在循环依赖
|
||||
* @en Whether circular dependencies exist
|
||||
*/
|
||||
hasCycles: boolean;
|
||||
|
||||
/**
|
||||
* @zh 循环依赖中的项目 ID
|
||||
* @en IDs of items in circular dependencies
|
||||
*/
|
||||
cycleIds?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 依赖验证结果
|
||||
* @en Dependency validation result
|
||||
*/
|
||||
export interface DependencyValidationResult {
|
||||
/**
|
||||
* @zh 是否验证通过
|
||||
* @en Whether validation passed
|
||||
*/
|
||||
valid: boolean;
|
||||
|
||||
/**
|
||||
* @zh 缺失依赖的映射(项目 ID -> 缺失的依赖 ID 列表)
|
||||
* @en Map of missing dependencies (item ID -> missing dependency IDs)
|
||||
*/
|
||||
missingDependencies: Map<string, string[]>;
|
||||
|
||||
/**
|
||||
* @zh 循环依赖的项目 ID
|
||||
* @en IDs involved in circular dependencies
|
||||
*/
|
||||
circularDependencies?: string[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 依赖 ID 解析 | Dependency ID Resolution
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 解析依赖 ID(短 ID 转完整包名)
|
||||
* @en Resolve dependency ID (short ID to full package name)
|
||||
*
|
||||
* @example
|
||||
* resolveDependencyId('sprite') // '@esengine/sprite'
|
||||
* resolveDependencyId('@esengine/sprite') // '@esengine/sprite'
|
||||
* resolveDependencyId('@dimforge/rapier2d') // '@dimforge/rapier2d'
|
||||
*/
|
||||
export function resolveDependencyId(depId: string, scope = '@esengine'): string {
|
||||
if (depId.startsWith('@')) {
|
||||
return depId;
|
||||
}
|
||||
return `${scope}/${depId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从完整包名提取短 ID
|
||||
* @en Extract short ID from full package name
|
||||
*
|
||||
* @example
|
||||
* extractShortId('@esengine/sprite') // 'sprite'
|
||||
* extractShortId('@esengine/ecs-framework') // 'core' (特殊映射)
|
||||
*/
|
||||
export function extractShortId(packageName: string): string {
|
||||
if (packageName.startsWith('@esengine/')) {
|
||||
const name = packageName.slice(10);
|
||||
if (name === 'ecs-framework') return 'core';
|
||||
if (name === 'ecs-framework-math') return 'math';
|
||||
return name;
|
||||
}
|
||||
|
||||
const scopeMatch = packageName.match(/^@[^/]+\/(.+)$/);
|
||||
if (scopeMatch) {
|
||||
return scopeMatch[1];
|
||||
}
|
||||
|
||||
return packageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从短 ID 获取完整包名
|
||||
* @en Get full package name from short ID
|
||||
*
|
||||
* @example
|
||||
* getPackageName('core') // '@esengine/ecs-framework'
|
||||
* getPackageName('sprite') // '@esengine/sprite'
|
||||
*/
|
||||
export function getPackageName(shortId: string): string {
|
||||
if (shortId === 'core') return '@esengine/ecs-framework';
|
||||
if (shortId === 'math') return '@esengine/ecs-framework-math';
|
||||
return `@esengine/${shortId}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 拓扑排序 | Topological Sort
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 使用 Kahn 算法进行拓扑排序
|
||||
* @en Topological sort using Kahn's algorithm
|
||||
*
|
||||
* @zh Kahn 算法优势:
|
||||
* - 能够检测循环依赖
|
||||
* - 返回所有循环中的节点
|
||||
* - 时间复杂度 O(V + E)
|
||||
*/
|
||||
function kahnSort<T extends IDependable>(
|
||||
items: T[],
|
||||
resolveId: (id: string) => string
|
||||
): TopologicalSortResult<T> {
|
||||
const itemMap = new Map<string, T>();
|
||||
const graph = new Map<string, Set<string>>();
|
||||
const inDegree = new Map<string, number>();
|
||||
|
||||
// 构建节点映射
|
||||
for (const item of items) {
|
||||
itemMap.set(item.id, item);
|
||||
graph.set(item.id, new Set());
|
||||
inDegree.set(item.id, 0);
|
||||
}
|
||||
|
||||
// 构建边(依赖 -> 被依赖者)
|
||||
for (const item of items) {
|
||||
for (const dep of item.dependencies || []) {
|
||||
const depId = resolveId(dep);
|
||||
if (itemMap.has(depId)) {
|
||||
graph.get(depId)!.add(item.id);
|
||||
inDegree.set(item.id, (inDegree.get(item.id) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 收集入度为 0 的节点
|
||||
const queue: string[] = [];
|
||||
for (const [id, degree] of inDegree) {
|
||||
if (degree === 0) {
|
||||
queue.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
// BFS 处理
|
||||
const sorted: T[] = [];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
sorted.push(itemMap.get(current)!);
|
||||
|
||||
for (const neighbor of graph.get(current) || []) {
|
||||
const newDegree = (inDegree.get(neighbor) || 0) - 1;
|
||||
inDegree.set(neighbor, newDegree);
|
||||
if (newDegree === 0) {
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查循环依赖
|
||||
if (sorted.length !== items.length) {
|
||||
const cycleIds = items
|
||||
.filter(item => !sorted.includes(item))
|
||||
.map(item => item.id);
|
||||
return { sorted, hasCycles: true, cycleIds };
|
||||
}
|
||||
|
||||
return { sorted, hasCycles: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 使用 DFS 进行拓扑排序
|
||||
* @en Topological sort using DFS
|
||||
*
|
||||
* @zh DFS 算法特点:
|
||||
* - 实现简单
|
||||
* - 递归方式,栈溢出风险(极端情况)
|
||||
*/
|
||||
function dfsSort<T extends IDependable>(
|
||||
items: T[],
|
||||
resolveId: (id: string) => string
|
||||
): TopologicalSortResult<T> {
|
||||
const itemMap = new Map<string, T>();
|
||||
for (const item of items) {
|
||||
itemMap.set(item.id, item);
|
||||
}
|
||||
|
||||
const sorted: T[] = [];
|
||||
const visited = new Set<string>();
|
||||
const visiting = new Set<string>(); // 用于检测循环
|
||||
const cycleIds: string[] = [];
|
||||
|
||||
const visit = (item: T): boolean => {
|
||||
if (visited.has(item.id)) return true;
|
||||
if (visiting.has(item.id)) {
|
||||
cycleIds.push(item.id);
|
||||
return false; // 发现循环
|
||||
}
|
||||
|
||||
visiting.add(item.id);
|
||||
|
||||
for (const dep of item.dependencies || []) {
|
||||
const depId = resolveId(dep);
|
||||
const depItem = itemMap.get(depId);
|
||||
if (depItem && !visit(depItem)) {
|
||||
cycleIds.push(item.id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
visiting.delete(item.id);
|
||||
visited.add(item.id);
|
||||
sorted.push(item);
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const item of items) {
|
||||
if (!visited.has(item.id)) {
|
||||
visit(item);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sorted,
|
||||
hasCycles: cycleIds.length > 0,
|
||||
cycleIds: cycleIds.length > 0 ? [...new Set(cycleIds)] : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 拓扑排序(统一入口)
|
||||
* @en Topological sort (unified entry)
|
||||
*
|
||||
* @zh 按依赖关系对项目进行排序,确保被依赖的项目在前
|
||||
* @en Sort items by dependencies, ensuring dependencies come first
|
||||
*
|
||||
* @param items - @zh 待排序的项目列表 @en Items to sort
|
||||
* @param options - @zh 排序选项 @en Sort options
|
||||
* @returns @zh 排序结果 @en Sort result
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const plugins = [
|
||||
* { id: '@esengine/sprite', dependencies: ['engine-core'] },
|
||||
* { id: '@esengine/engine-core', dependencies: [] },
|
||||
* { id: '@esengine/tilemap', dependencies: ['sprite'] }
|
||||
* ];
|
||||
*
|
||||
* const result = topologicalSort(plugins);
|
||||
* // result.sorted: [engine-core, sprite, tilemap]
|
||||
* ```
|
||||
*/
|
||||
export function topologicalSort<T extends IDependable>(
|
||||
items: T[],
|
||||
options: TopologicalSortOptions = {}
|
||||
): TopologicalSortResult<T> {
|
||||
const {
|
||||
algorithm = 'kahn',
|
||||
detectCycles = true,
|
||||
resolveId = resolveDependencyId
|
||||
} = options;
|
||||
|
||||
if (items.length === 0) {
|
||||
return { sorted: [], hasCycles: false };
|
||||
}
|
||||
|
||||
const result = algorithm === 'kahn'
|
||||
? kahnSort(items, resolveId)
|
||||
: dfsSort(items, resolveId);
|
||||
|
||||
if (result.hasCycles && detectCycles) {
|
||||
logger.warn(`Circular dependency detected among: ${result.cycleIds?.join(', ')}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 依赖验证 | Dependency Validation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @zh 验证依赖完整性
|
||||
* @en Validate dependency completeness
|
||||
*
|
||||
* @zh 检查所有启用的项目的依赖是否都已启用
|
||||
* @en Check if all dependencies of enabled items are also enabled
|
||||
*
|
||||
* @param items - @zh 所有项目 @en All items
|
||||
* @param enabledIds - @zh 已启用的项目 ID 集合 @en Set of enabled item IDs
|
||||
* @param options - @zh 选项 @en Options
|
||||
* @returns @zh 验证结果 @en Validation result
|
||||
*/
|
||||
export function validateDependencies<T extends IDependable>(
|
||||
items: T[],
|
||||
enabledIds: Set<string>,
|
||||
options: { resolveId?: (id: string) => string } = {}
|
||||
): DependencyValidationResult {
|
||||
const { resolveId = resolveDependencyId } = options;
|
||||
const missingDependencies = new Map<string, string[]>();
|
||||
|
||||
for (const item of items) {
|
||||
if (!enabledIds.has(item.id)) continue;
|
||||
|
||||
const missing: string[] = [];
|
||||
for (const dep of item.dependencies || []) {
|
||||
const depId = resolveId(dep);
|
||||
if (!enabledIds.has(depId)) {
|
||||
missing.push(depId);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
missingDependencies.set(item.id, missing);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查循环依赖
|
||||
const enabledItems = items.filter(item => enabledIds.has(item.id));
|
||||
const sortResult = topologicalSort(enabledItems, { resolveId });
|
||||
|
||||
return {
|
||||
valid: missingDependencies.size === 0 && !sortResult.hasCycles,
|
||||
missingDependencies,
|
||||
circularDependencies: sortResult.cycleIds
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取项目的所有依赖(包括传递依赖)
|
||||
* @en Get all dependencies of an item (including transitive)
|
||||
*
|
||||
* @param itemId - @zh 项目 ID @en Item ID
|
||||
* @param items - @zh 所有项目 @en All items
|
||||
* @param options - @zh 选项 @en Options
|
||||
* @returns @zh 所有依赖 ID 的集合 @en Set of all dependency IDs
|
||||
*/
|
||||
export function getAllDependencies<T extends IDependable>(
|
||||
itemId: string,
|
||||
items: T[],
|
||||
options: { resolveId?: (id: string) => string } = {}
|
||||
): Set<string> {
|
||||
const { resolveId = resolveDependencyId } = options;
|
||||
const itemMap = new Map<string, T>();
|
||||
for (const item of items) {
|
||||
itemMap.set(item.id, item);
|
||||
}
|
||||
|
||||
const allDeps = new Set<string>();
|
||||
const visited = new Set<string>();
|
||||
|
||||
const collect = (id: string) => {
|
||||
if (visited.has(id)) return;
|
||||
visited.add(id);
|
||||
|
||||
const item = itemMap.get(id);
|
||||
if (!item) return;
|
||||
|
||||
for (const dep of item.dependencies || []) {
|
||||
const depId = resolveId(dep);
|
||||
allDeps.add(depId);
|
||||
collect(depId);
|
||||
}
|
||||
};
|
||||
|
||||
collect(itemId);
|
||||
return allDeps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取依赖于指定项目的所有项目(反向依赖)
|
||||
* @en Get all items that depend on the specified item (reverse dependencies)
|
||||
*
|
||||
* @param itemId - @zh 项目 ID @en Item ID
|
||||
* @param items - @zh 所有项目 @en All items
|
||||
* @param options - @zh 选项 @en Options
|
||||
* @returns @zh 所有依赖此项目的 ID 集合 @en Set of IDs that depend on this item
|
||||
*/
|
||||
export function getReverseDependencies<T extends IDependable>(
|
||||
itemId: string,
|
||||
items: T[],
|
||||
options: { resolveId?: (id: string) => string } = {}
|
||||
): Set<string> {
|
||||
const { resolveId = resolveDependencyId } = options;
|
||||
const reverseDeps = new Set<string>();
|
||||
|
||||
for (const item of items) {
|
||||
for (const dep of item.dependencies || []) {
|
||||
const depId = resolveId(dep);
|
||||
if (depId === itemId) {
|
||||
reverseDeps.add(item.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reverseDeps;
|
||||
}
|
||||
22
packages/engine/runtime-core/src/utils/index.ts
Normal file
22
packages/engine/runtime-core/src/utils/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @zh 运行时核心工具模块
|
||||
* @en Runtime Core Utilities
|
||||
*/
|
||||
|
||||
export {
|
||||
// 类型
|
||||
type IDependable,
|
||||
type TopologicalSortOptions,
|
||||
type TopologicalSortResult,
|
||||
type DependencyValidationResult,
|
||||
// 依赖 ID 解析
|
||||
resolveDependencyId,
|
||||
extractShortId,
|
||||
getPackageName,
|
||||
// 拓扑排序
|
||||
topologicalSort,
|
||||
// 依赖验证
|
||||
validateDependencies,
|
||||
getAllDependencies,
|
||||
getReverseDependencies
|
||||
} from './DependencyUtils';
|
||||
12
packages/engine/runtime-core/tsconfig.build.json
Normal file
12
packages/engine/runtime-core/tsconfig.build.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
29
packages/engine/runtime-core/tsconfig.json
Normal file
29
packages/engine/runtime-core/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../framework/core"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/ecs-engine-bindgen"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/engine-core"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/asset-system"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/engine/runtime-core/tsup.config.ts
Normal file
7
packages/engine/runtime-core/tsup.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
Reference in New Issue
Block a user