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:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

File diff suppressed because it is too large Load Diff

View 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;
}
}

View 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;
}

View 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();
}

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

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

View 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
};
}

View 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();
}

View 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 特性:
* - 所有游戏逻辑系统禁用(物理、行为树、动画)
* - 显示编辑器 UIGrid、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;
}

View 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');

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
/**
* Platform Adapters
* 平台适配器
*/
export {
BrowserPlatformAdapter,
BrowserPathResolver,
type BrowserPlatformConfig,
type BrowserPathResolveMode
} from './BrowserPlatformAdapter';
export { EditorPlatformAdapter, EditorPathResolver, type EditorPlatformConfig } from './EditorPlatformAdapter';

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

View File

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

View 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;
}
}

View 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;
}

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