Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级 * feat(core): 层级系统重构与UI变换矩阵修复 * refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题 * fix(physics): 修复跨包组件类引用问题 * feat: 统一运行时架构与浏览器运行支持 * feat(asset): 实现浏览器运行时资产加载系统 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误 * test: 补齐核心模块测试用例,修复CI构建配置 * fix: 修复测试用例中的类型错误和断言问题 * fix: 修复 turbo build:npm 任务的依赖顺序问题 * fix: 修复 CI 构建错误并优化构建性能
This commit is contained in:
41
packages/runtime-core/package.json
Normal file
41
packages/runtime-core/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@esengine/runtime-core",
|
||||
"version": "1.0.0",
|
||||
"description": "Runtime core - plugin management and system initialization",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/ecs-engine-bindgen": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/asset-system": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"runtime",
|
||||
"plugin"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT"
|
||||
}
|
||||
824
packages/runtime-core/src/GameRuntime.ts
Normal file
824
packages/runtime-core/src/GameRuntime.ts
Normal file
@@ -0,0 +1,824 @@
|
||||
/**
|
||||
* Unified Game Runtime
|
||||
* 统一游戏运行时
|
||||
*
|
||||
* 这是编辑器预览和独立运行的统一入口点
|
||||
* This is the unified entry point for editor preview and standalone runtime
|
||||
*/
|
||||
|
||||
import { Core, Scene, SceneSerializer, HierarchySystem } from '@esengine/ecs-framework';
|
||||
import { EngineBridge, EngineRenderSystem, CameraSystem } from '@esengine/ecs-engine-bindgen';
|
||||
import { TransformComponent, TransformSystem } from '@esengine/engine-core';
|
||||
import { AssetManager, EngineIntegration } from '@esengine/asset-system';
|
||||
import {
|
||||
runtimePluginManager,
|
||||
type SystemContext,
|
||||
type IRuntimeModule
|
||||
} from './PluginManager';
|
||||
import {
|
||||
loadEnabledPlugins,
|
||||
type PluginPackageInfo,
|
||||
type ProjectPluginConfig
|
||||
} from './PluginLoader';
|
||||
import {
|
||||
BUILTIN_PLUGIN_PACKAGES,
|
||||
mergeProjectConfig,
|
||||
type ProjectConfig
|
||||
} from './ProjectConfig';
|
||||
import type { IPlatformAdapter, PlatformAdapterConfig } from './IPlatformAdapter';
|
||||
|
||||
/**
|
||||
* 运行时配置
|
||||
* Runtime configuration
|
||||
*/
|
||||
export interface GameRuntimeConfig {
|
||||
/** 平台适配器 */
|
||||
platform: IPlatformAdapter;
|
||||
/** 项目配置 */
|
||||
projectConfig?: Partial<ProjectConfig>;
|
||||
/** Canvas ID */
|
||||
canvasId: string;
|
||||
/** 初始宽度 */
|
||||
width?: number;
|
||||
/** 初始高度 */
|
||||
height?: number;
|
||||
/** 是否自动启动渲染循环 */
|
||||
autoStartRenderLoop?: boolean;
|
||||
/** UI 画布尺寸 */
|
||||
uiCanvasSize?: { width: number; height: number };
|
||||
/**
|
||||
* 跳过内部插件加载
|
||||
* 编辑器模式下,插件由 editor-core 的 PluginManager 管理
|
||||
* Skip internal plugin loading - editor mode uses editor-core's PluginManager
|
||||
*/
|
||||
skipPluginLoading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时状态
|
||||
* Runtime state
|
||||
*/
|
||||
export interface RuntimeState {
|
||||
initialized: boolean;
|
||||
running: boolean;
|
||||
paused: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一游戏运行时
|
||||
* Unified Game Runtime
|
||||
*
|
||||
* 提供编辑器预览和独立运行的统一实现
|
||||
* Provides unified implementation for editor preview and standalone runtime
|
||||
*/
|
||||
export class GameRuntime {
|
||||
private _platform: IPlatformAdapter;
|
||||
private _bridge: EngineBridge | null = null;
|
||||
private _scene: Scene | null = null;
|
||||
private _renderSystem: EngineRenderSystem | null = null;
|
||||
private _cameraSystem: CameraSystem | null = null;
|
||||
private _assetManager: AssetManager | null = null;
|
||||
private _engineIntegration: EngineIntegration | null = null;
|
||||
private _projectConfig: ProjectConfig;
|
||||
private _config: GameRuntimeConfig;
|
||||
|
||||
private _state: RuntimeState = {
|
||||
initialized: false,
|
||||
running: false,
|
||||
paused: false
|
||||
};
|
||||
|
||||
private _animationFrameId: number | null = null;
|
||||
private _lastTime = 0;
|
||||
|
||||
// 系统上下文,供插件使用
|
||||
private _systemContext: SystemContext | null = null;
|
||||
|
||||
// 场景快照(用于编辑器预览后恢复)
|
||||
private _sceneSnapshot: string | null = null;
|
||||
|
||||
// Gizmo 注册表注入函数
|
||||
private _gizmoDataProvider?: (component: any, entity: any, isSelected: boolean) => any;
|
||||
private _hasGizmoProvider?: (component: any) => boolean;
|
||||
|
||||
constructor(config: GameRuntimeConfig) {
|
||||
this._config = config;
|
||||
this._platform = config.platform;
|
||||
this._projectConfig = mergeProjectConfig(config.projectConfig || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行时状态
|
||||
*/
|
||||
get state(): RuntimeState {
|
||||
return { ...this._state };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取场景
|
||||
*/
|
||||
get scene(): Scene | null {
|
||||
return this._scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取引擎桥接
|
||||
*/
|
||||
get bridge(): EngineBridge | null {
|
||||
return this._bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取渲染系统
|
||||
*/
|
||||
get renderSystem(): EngineRenderSystem | null {
|
||||
return this._renderSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资产管理器
|
||||
*/
|
||||
get assetManager(): AssetManager | null {
|
||||
return this._assetManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取引擎集成
|
||||
*/
|
||||
get engineIntegration(): EngineIntegration | null {
|
||||
return this._engineIntegration;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统上下文
|
||||
*/
|
||||
get systemContext(): SystemContext | null {
|
||||
return this._systemContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新系统上下文(用于编辑器模式下同步外部创建的系统引用)
|
||||
* Update system context (for syncing externally created system references in editor mode)
|
||||
*/
|
||||
updateSystemContext(updates: Partial<SystemContext>): void {
|
||||
if (this._systemContext) {
|
||||
Object.assign(this._systemContext, updates);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台适配器
|
||||
*/
|
||||
get platform(): IPlatformAdapter {
|
||||
return this._platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化运行时
|
||||
* Initialize runtime
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this._state.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 初始化平台
|
||||
await this._platform.initialize({
|
||||
canvasId: this._config.canvasId,
|
||||
width: this._config.width,
|
||||
height: this._config.height,
|
||||
isEditor: this._platform.isEditorMode()
|
||||
});
|
||||
|
||||
// 2. 获取 WASM 模块并创建引擎桥接
|
||||
const wasmModule = await this._platform.getWasmModule();
|
||||
this._bridge = new EngineBridge({
|
||||
canvasId: this._config.canvasId,
|
||||
width: this._config.width,
|
||||
height: this._config.height
|
||||
});
|
||||
await this._bridge.initializeWithModule(wasmModule);
|
||||
|
||||
// 3. 设置路径解析器
|
||||
this._bridge.setPathResolver((path: string) => {
|
||||
return this._platform.pathResolver.resolve(path);
|
||||
});
|
||||
|
||||
// 4. 初始化 ECS Core
|
||||
if (!Core.Instance) {
|
||||
Core.create({ debug: false });
|
||||
}
|
||||
|
||||
// 5. 创建或获取场景
|
||||
if (Core.scene) {
|
||||
this._scene = Core.scene as Scene;
|
||||
} else {
|
||||
this._scene = new Scene({ name: 'GameScene' });
|
||||
Core.setScene(this._scene);
|
||||
}
|
||||
|
||||
// 6. 添加基础系统
|
||||
this._scene.addSystem(new HierarchySystem());
|
||||
this._scene.addSystem(new TransformSystem());
|
||||
|
||||
this._cameraSystem = new CameraSystem(this._bridge);
|
||||
this._scene.addSystem(this._cameraSystem);
|
||||
|
||||
this._renderSystem = new EngineRenderSystem(this._bridge, TransformComponent);
|
||||
|
||||
// 7. 设置 UI 画布尺寸
|
||||
if (this._config.uiCanvasSize) {
|
||||
this._renderSystem.setUICanvasSize(
|
||||
this._config.uiCanvasSize.width,
|
||||
this._config.uiCanvasSize.height
|
||||
);
|
||||
} else {
|
||||
this._renderSystem.setUICanvasSize(1920, 1080);
|
||||
}
|
||||
|
||||
// 8. 创建资产系统
|
||||
this._assetManager = new AssetManager();
|
||||
this._engineIntegration = new EngineIntegration(this._assetManager, this._bridge);
|
||||
|
||||
// 9. 加载并初始化插件(编辑器模式下跳过,由 editor-core 的 PluginManager 处理)
|
||||
if (!this._config.skipPluginLoading) {
|
||||
await this._initializePlugins();
|
||||
}
|
||||
|
||||
// 10. 创建系统上下文
|
||||
this._systemContext = {
|
||||
isEditor: this._platform.isEditorMode(),
|
||||
engineBridge: this._bridge,
|
||||
renderSystem: this._renderSystem,
|
||||
assetManager: this._assetManager
|
||||
};
|
||||
|
||||
// 11. 让插件创建系统(编辑器模式下跳过,由 EngineService.initializeModuleSystems 处理)
|
||||
if (!this._config.skipPluginLoading) {
|
||||
runtimePluginManager.createSystemsForScene(this._scene, this._systemContext);
|
||||
}
|
||||
|
||||
// 11. 设置 UI 渲染数据提供者(如果有)
|
||||
if (this._systemContext.uiRenderProvider) {
|
||||
this._renderSystem.setUIRenderDataProvider(this._systemContext.uiRenderProvider);
|
||||
}
|
||||
|
||||
// 12. 添加渲染系统(在所有其他系统之后)
|
||||
this._scene.addSystem(this._renderSystem);
|
||||
|
||||
// 13. 启动默认 world
|
||||
const defaultWorld = Core.worldManager.getWorld('__default__');
|
||||
if (defaultWorld && !defaultWorld.isActive) {
|
||||
defaultWorld.start();
|
||||
}
|
||||
|
||||
// 14. 编辑器模式下的特殊处理
|
||||
if (this._platform.isEditorMode()) {
|
||||
// 禁用游戏逻辑系统
|
||||
this._disableGameLogicSystems();
|
||||
}
|
||||
|
||||
this._state.initialized = true;
|
||||
|
||||
// 15. 自动启动渲染循环
|
||||
if (this._config.autoStartRenderLoop !== false) {
|
||||
this._startRenderLoop();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[GameRuntime] Initialization failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载并初始化插件
|
||||
*/
|
||||
private async _initializePlugins(): Promise<void> {
|
||||
// 检查是否已有插件注册(静态导入场景)
|
||||
// Check if plugins are already registered (static import scenario)
|
||||
const hasPlugins = runtimePluginManager.getPlugins().length > 0;
|
||||
|
||||
if (!hasPlugins) {
|
||||
// 没有预注册的插件,尝试动态加载
|
||||
// No pre-registered plugins, try dynamic loading
|
||||
await loadEnabledPlugins(
|
||||
{ plugins: this._projectConfig.plugins },
|
||||
BUILTIN_PLUGIN_PACKAGES
|
||||
);
|
||||
}
|
||||
|
||||
// 初始化插件(注册组件和服务)
|
||||
await runtimePluginManager.initializeRuntime(Core.services);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用游戏逻辑系统(编辑器模式)
|
||||
*/
|
||||
private _disableGameLogicSystems(): void {
|
||||
const ctx = this._systemContext;
|
||||
if (!ctx) return;
|
||||
|
||||
// 这些系统由插件创建,通过 context 传递引用
|
||||
if (ctx.animatorSystem) {
|
||||
ctx.animatorSystem.enabled = false;
|
||||
}
|
||||
if (ctx.behaviorTreeSystem) {
|
||||
ctx.behaviorTreeSystem.enabled = false;
|
||||
}
|
||||
if (ctx.physicsSystem) {
|
||||
ctx.physicsSystem.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用游戏逻辑系统(预览/运行模式)
|
||||
*/
|
||||
private _enableGameLogicSystems(): void {
|
||||
const ctx = this._systemContext;
|
||||
if (!ctx) return;
|
||||
|
||||
if (ctx.animatorSystem) {
|
||||
ctx.animatorSystem.enabled = true;
|
||||
}
|
||||
if (ctx.behaviorTreeSystem) {
|
||||
ctx.behaviorTreeSystem.enabled = true;
|
||||
ctx.behaviorTreeSystem.startAllAutoStartTrees?.();
|
||||
}
|
||||
if (ctx.physicsSystem) {
|
||||
ctx.physicsSystem.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动渲染循环
|
||||
*/
|
||||
private _startRenderLoop(): void {
|
||||
if (this._animationFrameId !== null) {
|
||||
return;
|
||||
}
|
||||
this._lastTime = performance.now();
|
||||
this._renderLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染循环
|
||||
*/
|
||||
private _renderLoop = (): void => {
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - this._lastTime) / 1000;
|
||||
this._lastTime = currentTime;
|
||||
|
||||
// 更新 ECS
|
||||
Core.update(deltaTime);
|
||||
|
||||
this._animationFrameId = requestAnimationFrame(this._renderLoop);
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止渲染循环
|
||||
*/
|
||||
private _stopRenderLoop(): void {
|
||||
if (this._animationFrameId !== null) {
|
||||
cancelAnimationFrame(this._animationFrameId);
|
||||
this._animationFrameId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始运行(启用游戏逻辑)
|
||||
* Start running (enable game logic)
|
||||
*/
|
||||
start(): void {
|
||||
if (!this._state.initialized || this._state.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._state.running = true;
|
||||
this._state.paused = false;
|
||||
|
||||
// 启用预览模式
|
||||
if (this._renderSystem) {
|
||||
this._renderSystem.setPreviewMode(true);
|
||||
}
|
||||
|
||||
// 启用游戏逻辑系统
|
||||
this._enableGameLogicSystems();
|
||||
|
||||
// 绑定 UI 输入
|
||||
const ctx = this._systemContext;
|
||||
if (ctx?.uiInputSystem && this._config.canvasId) {
|
||||
const canvas = document.getElementById(this._config.canvasId) as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
ctx.uiInputSystem.bindToCanvas(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
// 确保渲染循环在运行
|
||||
this._startRenderLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停运行
|
||||
* Pause running
|
||||
*/
|
||||
pause(): void {
|
||||
if (!this._state.running || this._state.paused) {
|
||||
return;
|
||||
}
|
||||
this._state.paused = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复运行
|
||||
* Resume running
|
||||
*/
|
||||
resume(): void {
|
||||
if (!this._state.running || !this._state.paused) {
|
||||
return;
|
||||
}
|
||||
this._state.paused = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止运行(禁用游戏逻辑)
|
||||
* Stop running (disable game logic)
|
||||
*/
|
||||
stop(): void {
|
||||
if (!this._state.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._state.running = false;
|
||||
this._state.paused = false;
|
||||
|
||||
// 禁用预览模式
|
||||
if (this._renderSystem) {
|
||||
this._renderSystem.setPreviewMode(false);
|
||||
}
|
||||
|
||||
// 解绑 UI 输入
|
||||
const ctx = this._systemContext;
|
||||
if (ctx?.uiInputSystem) {
|
||||
ctx.uiInputSystem.unbind?.();
|
||||
}
|
||||
|
||||
// 禁用游戏逻辑系统
|
||||
this._disableGameLogicSystems();
|
||||
|
||||
// 重置物理系统
|
||||
if (ctx?.physicsSystem) {
|
||||
ctx.physicsSystem.reset?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单步执行
|
||||
* Step forward one frame
|
||||
*/
|
||||
step(): void {
|
||||
if (!this._state.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 启用系统执行一帧
|
||||
this._enableGameLogicSystems();
|
||||
Core.update(1 / 60);
|
||||
this._disableGameLogicSystems();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载场景数据
|
||||
* Load scene data
|
||||
*/
|
||||
async loadScene(sceneData: string | object): Promise<void> {
|
||||
if (!this._scene) {
|
||||
throw new Error('Scene not initialized');
|
||||
}
|
||||
|
||||
const jsonStr = typeof sceneData === 'string'
|
||||
? sceneData
|
||||
: JSON.stringify(sceneData);
|
||||
|
||||
SceneSerializer.deserialize(this._scene, jsonStr, {
|
||||
strategy: 'replace',
|
||||
preserveIds: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 URL 加载场景
|
||||
* Load scene from URL
|
||||
*/
|
||||
async loadSceneFromUrl(url: string): Promise<void> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load scene from ${url}: ${response.status}`);
|
||||
}
|
||||
const sceneJson = await response.text();
|
||||
await this.loadScene(sceneJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整视口大小
|
||||
* Resize viewport
|
||||
*/
|
||||
resize(width: number, height: number): void {
|
||||
if (this._bridge) {
|
||||
this._bridge.resize(width, height);
|
||||
}
|
||||
this._platform.resize(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置相机
|
||||
* Set camera
|
||||
*/
|
||||
setCamera(config: { x: number; y: number; zoom: number; rotation?: number }): void {
|
||||
if (this._bridge) {
|
||||
this._bridge.setCamera({
|
||||
x: config.x,
|
||||
y: config.y,
|
||||
zoom: config.zoom,
|
||||
rotation: config.rotation ?? 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相机状态
|
||||
* Get camera state
|
||||
*/
|
||||
getCamera(): { x: number; y: number; zoom: number; rotation: number } {
|
||||
if (this._bridge) {
|
||||
return this._bridge.getCamera();
|
||||
}
|
||||
return { x: 0, y: 0, zoom: 1, rotation: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置网格显示
|
||||
* Set grid visibility
|
||||
*/
|
||||
setShowGrid(show: boolean): void {
|
||||
if (this._bridge) {
|
||||
this._bridge.setShowGrid(show);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Gizmo 显示
|
||||
* Set gizmo visibility
|
||||
*/
|
||||
setShowGizmos(show: boolean): void {
|
||||
if (this._renderSystem) {
|
||||
this._renderSystem.setShowGizmos(show);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置清除颜色
|
||||
* Set clear color
|
||||
*/
|
||||
setClearColor(r: number, g: number, b: number, a: number = 1.0): void {
|
||||
if (this._bridge) {
|
||||
this._bridge.setClearColor(r, g, b, a);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
* Get stats
|
||||
*/
|
||||
getStats(): { fps: number; drawCalls: number; spriteCount: number } {
|
||||
if (!this._renderSystem) {
|
||||
return { fps: 0, drawCalls: 0, spriteCount: 0 };
|
||||
}
|
||||
|
||||
const engineStats = this._renderSystem.getStats();
|
||||
return {
|
||||
fps: engineStats?.fps ?? 0,
|
||||
drawCalls: engineStats?.drawCalls ?? 0,
|
||||
spriteCount: this._renderSystem.spriteCount
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 编辑器特有功能 =====
|
||||
// ===== Editor-specific features =====
|
||||
|
||||
/**
|
||||
* 设置 Gizmo 注册表(编辑器模式)
|
||||
* Set gizmo registry (editor mode)
|
||||
*/
|
||||
setGizmoRegistry(
|
||||
gizmoDataProvider: (component: any, entity: any, isSelected: boolean) => any,
|
||||
hasGizmoProvider: (component: any) => boolean
|
||||
): void {
|
||||
this._gizmoDataProvider = gizmoDataProvider;
|
||||
this._hasGizmoProvider = hasGizmoProvider;
|
||||
|
||||
if (this._renderSystem) {
|
||||
this._renderSystem.setGizmoRegistry(gizmoDataProvider, hasGizmoProvider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选中的实体 ID(编辑器模式)
|
||||
* Set selected entity IDs (editor mode)
|
||||
*/
|
||||
setSelectedEntityIds(ids: number[]): void {
|
||||
if (this._renderSystem) {
|
||||
this._renderSystem.setSelectedEntityIds(ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置变换工具模式(编辑器模式)
|
||||
* Set transform tool mode (editor mode)
|
||||
*/
|
||||
setTransformMode(mode: 'select' | 'move' | 'rotate' | 'scale'): void {
|
||||
if (this._renderSystem) {
|
||||
this._renderSystem.setTransformMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取变换工具模式
|
||||
* Get transform tool mode
|
||||
*/
|
||||
getTransformMode(): 'select' | 'move' | 'rotate' | 'scale' {
|
||||
return this._renderSystem?.getTransformMode() ?? 'select';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 UI 画布尺寸
|
||||
* Set UI canvas size
|
||||
*/
|
||||
setUICanvasSize(width: number, height: number): void {
|
||||
if (this._renderSystem) {
|
||||
this._renderSystem.setUICanvasSize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 UI 画布尺寸
|
||||
* Get UI canvas size
|
||||
*/
|
||||
getUICanvasSize(): { width: number; height: number } {
|
||||
return this._renderSystem?.getUICanvasSize() ?? { width: 0, height: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 UI 画布边界显示
|
||||
* Set UI canvas boundary visibility
|
||||
*/
|
||||
setShowUICanvasBoundary(show: boolean): void {
|
||||
if (this._renderSystem) {
|
||||
this._renderSystem.setShowUICanvasBoundary(show);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 UI 画布边界显示状态
|
||||
* Get UI canvas boundary visibility
|
||||
*/
|
||||
getShowUICanvasBoundary(): boolean {
|
||||
return this._renderSystem?.getShowUICanvasBoundary() ?? true;
|
||||
}
|
||||
|
||||
// ===== 场景快照 API =====
|
||||
// ===== Scene Snapshot API =====
|
||||
|
||||
/**
|
||||
* 保存场景快照
|
||||
* Save scene snapshot
|
||||
*/
|
||||
saveSceneSnapshot(): boolean {
|
||||
if (!this._scene) {
|
||||
console.warn('[GameRuntime] Cannot save snapshot: no scene');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this._sceneSnapshot = SceneSerializer.serialize(this._scene, {
|
||||
format: 'json',
|
||||
pretty: false,
|
||||
includeMetadata: false
|
||||
}) as string;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[GameRuntime] Failed to save snapshot:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复场景快照
|
||||
* Restore scene snapshot
|
||||
*/
|
||||
async restoreSceneSnapshot(): Promise<boolean> {
|
||||
if (!this._scene || !this._sceneSnapshot) {
|
||||
console.warn('[GameRuntime] Cannot restore: no scene or snapshot');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 清除缓存
|
||||
const ctx = this._systemContext;
|
||||
if (ctx?.tilemapSystem) {
|
||||
ctx.tilemapSystem.clearCache?.();
|
||||
}
|
||||
|
||||
// 反序列化场景
|
||||
SceneSerializer.deserialize(this._scene, this._sceneSnapshot, {
|
||||
strategy: 'replace',
|
||||
preserveIds: true
|
||||
});
|
||||
|
||||
this._sceneSnapshot = null;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[GameRuntime] Failed to restore snapshot:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有快照
|
||||
* Check if snapshot exists
|
||||
*/
|
||||
hasSnapshot(): boolean {
|
||||
return this._sceneSnapshot !== null;
|
||||
}
|
||||
|
||||
// ===== 多视口 API =====
|
||||
// ===== Multi-viewport API =====
|
||||
|
||||
/**
|
||||
* 注册视口
|
||||
* Register viewport
|
||||
*/
|
||||
registerViewport(id: string, canvasId: string): void {
|
||||
if (this._bridge) {
|
||||
this._bridge.registerViewport(id, canvasId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销视口
|
||||
* Unregister viewport
|
||||
*/
|
||||
unregisterViewport(id: string): void {
|
||||
if (this._bridge) {
|
||||
this._bridge.unregisterViewport(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置活动视口
|
||||
* Set active viewport
|
||||
*/
|
||||
setActiveViewport(id: string): boolean {
|
||||
if (this._bridge) {
|
||||
return this._bridge.setActiveViewport(id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
* Dispose resources
|
||||
*/
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
this._stopRenderLoop();
|
||||
|
||||
if (this._assetManager) {
|
||||
this._assetManager.dispose();
|
||||
this._assetManager = null;
|
||||
}
|
||||
|
||||
this._engineIntegration = null;
|
||||
this._scene = null;
|
||||
|
||||
if (this._bridge) {
|
||||
this._bridge.dispose();
|
||||
this._bridge = null;
|
||||
}
|
||||
|
||||
this._renderSystem = null;
|
||||
this._cameraSystem = null;
|
||||
this._systemContext = null;
|
||||
this._platform.dispose();
|
||||
|
||||
this._state.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建游戏运行时实例
|
||||
* Create game runtime instance
|
||||
*/
|
||||
export function createGameRuntime(config: GameRuntimeConfig): GameRuntime {
|
||||
return new GameRuntime(config);
|
||||
}
|
||||
152
packages/runtime-core/src/IPlatformAdapter.ts
Normal file
152
packages/runtime-core/src/IPlatformAdapter.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Platform Adapter Interface
|
||||
* 平台适配器接口
|
||||
*
|
||||
* 定义不同平台(编辑器、浏览器、原生)需要实现的适配器接口
|
||||
* Defines the adapter interface that different platforms need to implement
|
||||
*/
|
||||
|
||||
/**
|
||||
* 资源路径解析器
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
118
packages/runtime-core/src/PluginLoader.ts
Normal file
118
packages/runtime-core/src/PluginLoader.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { runtimePluginManager, type IPlugin } from './PluginManager';
|
||||
|
||||
export interface PluginPackageInfo {
|
||||
plugin: boolean;
|
||||
pluginExport: string;
|
||||
category?: string;
|
||||
isEnginePlugin?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
enabled: boolean;
|
||||
options?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ProjectPluginConfig {
|
||||
plugins: Record<string, PluginConfig>;
|
||||
}
|
||||
|
||||
interface LoadedPluginInfo {
|
||||
id: string;
|
||||
plugin: IPlugin;
|
||||
packageInfo: PluginPackageInfo;
|
||||
}
|
||||
|
||||
const loadedPlugins = new Map<string, LoadedPluginInfo>();
|
||||
|
||||
/**
|
||||
* 从模块动态加载插件
|
||||
* @param packageId 包 ID,如 '@esengine/sprite'
|
||||
* @param packageInfo 包的 esengine 配置
|
||||
*/
|
||||
export async function loadPlugin(
|
||||
packageId: string,
|
||||
packageInfo: PluginPackageInfo
|
||||
): Promise<IPlugin | null> {
|
||||
if (loadedPlugins.has(packageId)) {
|
||||
return loadedPlugins.get(packageId)!.plugin;
|
||||
}
|
||||
|
||||
try {
|
||||
const module = await import(/* @vite-ignore */ packageId);
|
||||
const exportName = packageInfo.pluginExport || 'default';
|
||||
const plugin = module[exportName] as IPlugin;
|
||||
|
||||
if (!plugin || !plugin.descriptor) {
|
||||
console.warn(`[PluginLoader] Invalid plugin export from ${packageId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
loadedPlugins.set(packageId, {
|
||||
id: packageId,
|
||||
plugin,
|
||||
packageInfo
|
||||
});
|
||||
|
||||
return plugin;
|
||||
} catch (error) {
|
||||
console.error(`[PluginLoader] Failed to load plugin ${packageId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据项目配置加载所有启用的插件
|
||||
*/
|
||||
export async function loadEnabledPlugins(
|
||||
config: ProjectPluginConfig,
|
||||
packageInfoMap: Record<string, PluginPackageInfo>
|
||||
): Promise<void> {
|
||||
const sortedPlugins: Array<{ id: string; info: PluginPackageInfo }> = [];
|
||||
|
||||
for (const [id, pluginConfig] of Object.entries(config.plugins)) {
|
||||
if (!pluginConfig.enabled) continue;
|
||||
|
||||
const packageInfo = packageInfoMap[id];
|
||||
if (!packageInfo) {
|
||||
console.warn(`[PluginLoader] No package info for ${id}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
sortedPlugins.push({ id, info: packageInfo });
|
||||
}
|
||||
|
||||
// 引擎核心插件优先加载
|
||||
sortedPlugins.sort((a, b) => {
|
||||
if (a.info.isEnginePlugin && !b.info.isEnginePlugin) return -1;
|
||||
if (!a.info.isEnginePlugin && b.info.isEnginePlugin) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (const { id, info } of sortedPlugins) {
|
||||
const plugin = await loadPlugin(id, info);
|
||||
if (plugin) {
|
||||
runtimePluginManager.register(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册预加载的插件(用于已静态导入的插件)
|
||||
*/
|
||||
export function registerStaticPlugin(plugin: IPlugin): void {
|
||||
runtimePluginManager.register(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已加载的插件列表
|
||||
*/
|
||||
export function getLoadedPlugins(): IPlugin[] {
|
||||
return Array.from(loadedPlugins.values()).map(info => info.plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置插件加载器状态
|
||||
*/
|
||||
export function resetPluginLoader(): void {
|
||||
loadedPlugins.clear();
|
||||
}
|
||||
166
packages/runtime-core/src/PluginManager.ts
Normal file
166
packages/runtime-core/src/PluginManager.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Runtime Plugin Manager
|
||||
* 运行时插件管理器
|
||||
*/
|
||||
|
||||
import { ComponentRegistry, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import type { IScene } from '@esengine/ecs-framework';
|
||||
|
||||
export interface SystemContext {
|
||||
isEditor: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface PluginDescriptor {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
enabledByDefault?: boolean;
|
||||
isEnginePlugin?: boolean;
|
||||
}
|
||||
|
||||
export interface IRuntimeModule {
|
||||
registerComponents?(registry: typeof ComponentRegistry): void;
|
||||
registerServices?(services: ServiceContainer): void;
|
||||
createSystems?(scene: IScene, context: SystemContext): void;
|
||||
onSystemsCreated?(scene: IScene, context: SystemContext): void;
|
||||
onInitialize?(): Promise<void>;
|
||||
onDestroy?(): void;
|
||||
}
|
||||
|
||||
export interface IPlugin {
|
||||
readonly descriptor: PluginDescriptor;
|
||||
readonly runtimeModule?: IRuntimeModule;
|
||||
}
|
||||
|
||||
export class RuntimePluginManager {
|
||||
private _plugins = new Map<string, IPlugin>();
|
||||
private _enabledPlugins = new Set<string>();
|
||||
private _bInitialized = false;
|
||||
|
||||
register(plugin: IPlugin): void {
|
||||
const id = plugin.descriptor.id;
|
||||
if (this._plugins.has(id)) {
|
||||
return;
|
||||
}
|
||||
this._plugins.set(id, plugin);
|
||||
if (plugin.descriptor.enabledByDefault !== false) {
|
||||
this._enabledPlugins.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
enable(pluginId: string): void {
|
||||
this._enabledPlugins.add(pluginId);
|
||||
}
|
||||
|
||||
disable(pluginId: string): void {
|
||||
this._enabledPlugins.delete(pluginId);
|
||||
}
|
||||
|
||||
isEnabled(pluginId: string): boolean {
|
||||
return this._enabledPlugins.has(pluginId);
|
||||
}
|
||||
|
||||
loadConfig(config: { enabledPlugins: string[] }): void {
|
||||
this._enabledPlugins.clear();
|
||||
for (const id of config.enabledPlugins) {
|
||||
this._enabledPlugins.add(id);
|
||||
}
|
||||
// 始终启用引擎核心插件
|
||||
for (const [id, plugin] of this._plugins) {
|
||||
if (plugin.descriptor.isEnginePlugin) {
|
||||
this._enabledPlugins.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async initializeRuntime(services: ServiceContainer): Promise<void> {
|
||||
if (this._bInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [id, plugin] of this._plugins) {
|
||||
if (!this._enabledPlugins.has(id)) continue;
|
||||
const mod = plugin.runtimeModule;
|
||||
if (mod?.registerComponents) {
|
||||
try {
|
||||
mod.registerComponents(ComponentRegistry);
|
||||
} catch (e) {
|
||||
console.error(`[PluginManager] Failed to register components for ${id}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, plugin] of this._plugins) {
|
||||
if (!this._enabledPlugins.has(id)) continue;
|
||||
const mod = plugin.runtimeModule;
|
||||
if (mod?.registerServices) {
|
||||
try {
|
||||
mod.registerServices(services);
|
||||
} catch (e) {
|
||||
console.error(`[PluginManager] Failed to register services for ${id}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, plugin] of this._plugins) {
|
||||
if (!this._enabledPlugins.has(id)) continue;
|
||||
const mod = plugin.runtimeModule;
|
||||
if (mod?.onInitialize) {
|
||||
try {
|
||||
await mod.onInitialize();
|
||||
} catch (e) {
|
||||
console.error(`[PluginManager] Failed to initialize ${id}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._bInitialized = true;
|
||||
}
|
||||
|
||||
createSystemsForScene(scene: IScene, context: SystemContext): void {
|
||||
// Phase 1: 创建系统
|
||||
for (const [id, plugin] of this._plugins) {
|
||||
if (!this._enabledPlugins.has(id)) continue;
|
||||
const mod = plugin.runtimeModule;
|
||||
if (mod?.createSystems) {
|
||||
try {
|
||||
mod.createSystems(scene, context);
|
||||
} catch (e) {
|
||||
console.error(`[PluginManager] Failed to create systems for ${id}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: 连接跨插件依赖
|
||||
for (const [id, plugin] of this._plugins) {
|
||||
if (!this._enabledPlugins.has(id)) continue;
|
||||
const mod = plugin.runtimeModule;
|
||||
if (mod?.onSystemsCreated) {
|
||||
try {
|
||||
mod.onSystemsCreated(scene, context);
|
||||
} catch (e) {
|
||||
console.error(`[PluginManager] Failed to wire dependencies for ${id}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPlugins(): IPlugin[] {
|
||||
return Array.from(this._plugins.values());
|
||||
}
|
||||
|
||||
getPlugin(id: string): IPlugin | undefined {
|
||||
return this._plugins.get(id);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._plugins.clear();
|
||||
this._enabledPlugins.clear();
|
||||
this._bInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const runtimePluginManager = new RuntimePluginManager();
|
||||
128
packages/runtime-core/src/ProjectConfig.ts
Normal file
128
packages/runtime-core/src/ProjectConfig.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { PluginPackageInfo, PluginConfig } from './PluginLoader';
|
||||
|
||||
export interface ProjectConfig {
|
||||
name: string;
|
||||
version: string;
|
||||
plugins: Record<string, PluginConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内置引擎插件的包信息
|
||||
* 这些信息在构建时从各包的 package.json 中提取
|
||||
*/
|
||||
export const BUILTIN_PLUGIN_PACKAGES: Record<string, PluginPackageInfo> = {
|
||||
'@esengine/engine-core': {
|
||||
plugin: true,
|
||||
pluginExport: 'EnginePlugin',
|
||||
category: 'core',
|
||||
isEnginePlugin: true
|
||||
},
|
||||
'@esengine/camera': {
|
||||
plugin: true,
|
||||
pluginExport: 'CameraPlugin',
|
||||
category: 'core',
|
||||
isEnginePlugin: true
|
||||
},
|
||||
'@esengine/sprite': {
|
||||
plugin: true,
|
||||
pluginExport: 'SpritePlugin',
|
||||
category: 'rendering',
|
||||
isEnginePlugin: true
|
||||
},
|
||||
'@esengine/audio': {
|
||||
plugin: true,
|
||||
pluginExport: 'AudioPlugin',
|
||||
category: 'audio',
|
||||
isEnginePlugin: true
|
||||
},
|
||||
'@esengine/ui': {
|
||||
plugin: true,
|
||||
pluginExport: 'UIPlugin',
|
||||
category: 'ui'
|
||||
},
|
||||
'@esengine/tilemap': {
|
||||
plugin: true,
|
||||
pluginExport: 'TilemapPlugin',
|
||||
category: 'tilemap'
|
||||
},
|
||||
'@esengine/behavior-tree': {
|
||||
plugin: true,
|
||||
pluginExport: 'BehaviorTreePlugin',
|
||||
category: 'ai'
|
||||
},
|
||||
'@esengine/physics-rapier2d': {
|
||||
plugin: true,
|
||||
pluginExport: 'PhysicsPlugin',
|
||||
category: 'physics'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建默认项目配置
|
||||
*/
|
||||
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/tilemap': { enabled: false },
|
||||
'@esengine/behavior-tree': { enabled: false },
|
||||
'@esengine/physics-rapier2d': { enabled: false }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并用户配置与默认配置
|
||||
*/
|
||||
export function mergeProjectConfig(
|
||||
userConfig: Partial<ProjectConfig>
|
||||
): ProjectConfig {
|
||||
const defaultConfig = createDefaultProjectConfig();
|
||||
|
||||
return {
|
||||
name: userConfig.name || defaultConfig.name,
|
||||
version: userConfig.version || defaultConfig.version,
|
||||
plugins: {
|
||||
...defaultConfig.plugins,
|
||||
...userConfig.plugins
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从编辑器的 enabledPlugins 列表创建项目配置
|
||||
* Create project config from editor's enabledPlugins list
|
||||
*
|
||||
* @param enabledPlugins - 启用的插件 ID 列表 / List of enabled plugin IDs
|
||||
*/
|
||||
export function createProjectConfigFromEnabledList(
|
||||
enabledPlugins: string[]
|
||||
): ProjectConfig {
|
||||
const defaultConfig = createDefaultProjectConfig();
|
||||
|
||||
// 先禁用所有非核心插件
|
||||
// First disable all non-core plugins
|
||||
const plugins: Record<string, PluginConfig> = {};
|
||||
|
||||
for (const id of Object.keys(defaultConfig.plugins)) {
|
||||
const packageInfo = BUILTIN_PLUGIN_PACKAGES[id];
|
||||
// 核心插件始终启用
|
||||
// Core plugins are always enabled
|
||||
if (packageInfo?.isEnginePlugin) {
|
||||
plugins[id] = { enabled: true };
|
||||
} else {
|
||||
plugins[id] = { enabled: enabledPlugins.includes(id) };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultConfig,
|
||||
plugins
|
||||
};
|
||||
}
|
||||
66
packages/runtime-core/src/RuntimeBootstrap.ts
Normal file
66
packages/runtime-core/src/RuntimeBootstrap.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Runtime Bootstrap
|
||||
* 运行时启动器 - 提供通用的初始化流程
|
||||
*/
|
||||
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import type { IScene } from '@esengine/ecs-framework';
|
||||
import {
|
||||
runtimePluginManager,
|
||||
type IPlugin,
|
||||
type IRuntimeModule,
|
||||
type PluginDescriptor,
|
||||
type SystemContext
|
||||
} from './PluginManager';
|
||||
|
||||
export interface RuntimeConfig {
|
||||
enabledPlugins?: string[];
|
||||
isEditor?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建插件(简化工厂)
|
||||
*/
|
||||
export function createPlugin(
|
||||
descriptor: PluginDescriptor,
|
||||
runtimeModule: IRuntimeModule
|
||||
): IPlugin {
|
||||
return { descriptor, runtimeModule };
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册插件到运行时
|
||||
*/
|
||||
export function registerPlugin(plugin: IPlugin): 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.descriptor.id);
|
||||
}
|
||||
}
|
||||
|
||||
await runtimePluginManager.initializeRuntime(Core.services);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为场景创建系统
|
||||
*/
|
||||
export function createSystemsForScene(scene: IScene, context: SystemContext): void {
|
||||
runtimePluginManager.createSystemsForScene(scene, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置运行时(用于热重载等场景)
|
||||
*/
|
||||
export function resetRuntime(): void {
|
||||
runtimePluginManager.reset();
|
||||
}
|
||||
143
packages/runtime-core/src/adapters/BrowserPlatformAdapter.ts
Normal file
143
packages/runtime-core/src/adapters/BrowserPlatformAdapter.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Browser Platform Adapter
|
||||
* 浏览器平台适配器
|
||||
*
|
||||
* 用于独立浏览器运行时的平台适配器
|
||||
* Platform adapter for standalone browser runtime
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
IPathResolver,
|
||||
PlatformCapabilities,
|
||||
PlatformAdapterConfig
|
||||
} from '../IPlatformAdapter';
|
||||
|
||||
/**
|
||||
* 浏览器路径解析器
|
||||
* Browser path resolver
|
||||
*/
|
||||
export class BrowserPathResolver implements IPathResolver {
|
||||
private _baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string = '') {
|
||||
this._baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
resolve(path: string): string {
|
||||
// 如果已经是完整 URL,直接返回
|
||||
if (path.startsWith('http://') ||
|
||||
path.startsWith('https://') ||
|
||||
path.startsWith('data:') ||
|
||||
path.startsWith('blob:') ||
|
||||
path.startsWith('/asset?')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// 相对路径,添加资产请求前缀
|
||||
return `/asset?path=${encodeURIComponent(path)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新基础 URL
|
||||
*/
|
||||
setBaseUrl(baseUrl: string): void {
|
||||
this._baseUrl = baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器平台适配器配置
|
||||
*/
|
||||
export interface BrowserPlatformConfig {
|
||||
/** WASM 模块(预加载的)*/
|
||||
wasmModule?: any;
|
||||
/** WASM 模块加载器(异步加载)*/
|
||||
wasmModuleLoader?: () => Promise<any>;
|
||||
/** 资产基础 URL */
|
||||
assetBaseUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器平台适配器
|
||||
* 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 };
|
||||
|
||||
constructor(config: BrowserPlatformConfig = {}) {
|
||||
this._config = config;
|
||||
this._pathResolver = new BrowserPathResolver(config.assetBaseUrl || '');
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._canvas = null;
|
||||
}
|
||||
}
|
||||
185
packages/runtime-core/src/adapters/EditorPlatformAdapter.ts
Normal file
185
packages/runtime-core/src/adapters/EditorPlatformAdapter.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Editor Platform Adapter
|
||||
* 编辑器平台适配器
|
||||
*
|
||||
* 用于 Tauri 编辑器内嵌预览的平台适配器
|
||||
* Platform adapter for Tauri editor embedded preview
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
IPathResolver,
|
||||
PlatformCapabilities,
|
||||
PlatformAdapterConfig
|
||||
} from '../IPlatformAdapter';
|
||||
|
||||
/**
|
||||
* 编辑器路径解析器
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器平台适配器
|
||||
* 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;
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._canvas = null;
|
||||
}
|
||||
}
|
||||
7
packages/runtime-core/src/adapters/index.ts
Normal file
7
packages/runtime-core/src/adapters/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Platform Adapters
|
||||
* 平台适配器
|
||||
*/
|
||||
|
||||
export { BrowserPlatformAdapter, BrowserPathResolver, type BrowserPlatformConfig } from './BrowserPlatformAdapter';
|
||||
export { EditorPlatformAdapter, EditorPathResolver, type EditorPlatformConfig } from './EditorPlatformAdapter';
|
||||
72
packages/runtime-core/src/index.ts
Normal file
72
packages/runtime-core/src/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export {
|
||||
RuntimePluginManager,
|
||||
runtimePluginManager,
|
||||
type SystemContext,
|
||||
type PluginDescriptor,
|
||||
type IRuntimeModule,
|
||||
type IPlugin
|
||||
} from './PluginManager';
|
||||
|
||||
export {
|
||||
createPlugin,
|
||||
registerPlugin,
|
||||
initializeRuntime,
|
||||
createSystemsForScene,
|
||||
resetRuntime,
|
||||
type RuntimeConfig
|
||||
} from './RuntimeBootstrap';
|
||||
|
||||
export {
|
||||
loadPlugin,
|
||||
loadEnabledPlugins,
|
||||
registerStaticPlugin,
|
||||
getLoadedPlugins,
|
||||
resetPluginLoader,
|
||||
type PluginPackageInfo,
|
||||
type PluginConfig,
|
||||
type ProjectPluginConfig
|
||||
} from './PluginLoader';
|
||||
|
||||
export {
|
||||
BUILTIN_PLUGIN_PACKAGES,
|
||||
createDefaultProjectConfig,
|
||||
mergeProjectConfig,
|
||||
createProjectConfigFromEnabledList,
|
||||
type ProjectConfig
|
||||
} 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';
|
||||
|
||||
// Platform Adapters
|
||||
export {
|
||||
BrowserPlatformAdapter,
|
||||
BrowserPathResolver,
|
||||
type BrowserPlatformConfig,
|
||||
EditorPlatformAdapter,
|
||||
EditorPathResolver,
|
||||
type EditorPlatformConfig
|
||||
} from './adapters';
|
||||
|
||||
// Browser File System Service
|
||||
export {
|
||||
BrowserFileSystemService,
|
||||
createBrowserFileSystem,
|
||||
type AssetCatalog,
|
||||
type AssetCatalogEntry,
|
||||
type BrowserFileSystemOptions
|
||||
} from './services/BrowserFileSystemService';
|
||||
306
packages/runtime-core/src/services/BrowserFileSystemService.ts
Normal file
306
packages/runtime-core/src/services/BrowserFileSystemService.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Asset catalog entry
|
||||
*/
|
||||
export interface AssetCatalogEntry {
|
||||
guid: string;
|
||||
path: string;
|
||||
type: string;
|
||||
size: number;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset catalog loaded from JSON
|
||||
*/
|
||||
export interface AssetCatalog {
|
||||
version: string;
|
||||
createdAt: number;
|
||||
entries: Record<string, AssetCatalogEntry>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export class BrowserFileSystemService {
|
||||
private _baseUrl: string;
|
||||
private _catalogUrl: string;
|
||||
private _catalog: AssetCatalog | null = null;
|
||||
private _cache = new Map<string, string>();
|
||||
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;
|
||||
console.log('[BrowserFileSystem] Initialized with',
|
||||
Object.keys(this._catalog?.entries ?? {}).length, 'assets');
|
||||
} 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}`);
|
||||
}
|
||||
this._catalog = await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): AssetCatalogEntry | 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): AssetCatalogEntry[] {
|
||||
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(): AssetCatalog | 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);
|
||||
}
|
||||
12
packages/runtime-core/tsconfig.build.json
Normal file
12
packages/runtime-core/tsconfig.build.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
16
packages/runtime-core/tsconfig.json
Normal file
16
packages/runtime-core/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../ecs-engine-bindgen" },
|
||||
{ "path": "../engine-core" },
|
||||
{ "path": "../asset-system" }
|
||||
]
|
||||
}
|
||||
7
packages/runtime-core/tsup.config.ts
Normal file
7
packages/runtime-core/tsup.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
Reference in New Issue
Block a user