Files
esengine/packages/world-streaming-editor/src/WorldStreamingEditorModule.ts
YHH c3b7250f85 refactor(plugin): 重构插件系统架构,统一类型导入路径 (#296)
* refactor(plugin): 重构插件系统架构,统一类型导入路径

## 主要更改

### 新增 @esengine/plugin-types 包
- 提供打破循环依赖的最小类型定义
- 包含 ServiceToken, createServiceToken, PluginServiceRegistry, IEditorModuleBase

### engine-core 类型统一
- IPlugin<T> 泛型参数改为 T = unknown
- 所有运行时类型(IRuntimeModule, ModuleManifest, SystemContext)在此定义
- 新增 PluginServiceRegistry.ts 导出服务令牌相关类型

### editor-core 类型优化
- 重命名 IPluginLoader.ts 为 EditorModule.ts
- 新增 IEditorPlugin = IPlugin<IEditorModuleLoader> 类型别名
- 移除弃用别名 IPluginLoader, IRuntimeModuleLoader

### 编辑器插件更新
- 所有 9 个编辑器插件使用 IEditorPlugin 类型
- 统一从 editor-core 导入编辑器类型

### 服务令牌规范
- 各模块在 tokens.ts 中定义自己的服务接口和令牌
- 遵循"谁定义接口,谁导出 Token"原则

## 导入规范

| 场景 | 导入来源 |
|------|---------|
| 运行时模块 | @esengine/engine-core |
| 编辑器插件 | @esengine/editor-core |
| 服务令牌 | @esengine/engine-core |

* refactor(plugin): 完善服务令牌规范,统一运行时模块

## 更改内容

### 运行时模块优化
- particle: 使用 PluginServiceRegistry 获取依赖服务
- physics-rapier2d: 通过服务令牌注册/获取物理查询接口
- tilemap: 使用服务令牌获取物理系统依赖
- sprite: 导出服务令牌
- ui: 导出服务令牌
- behavior-tree: 使用服务令牌系统

### 资产系统增强
- IAssetManager 接口扩展
- 加载器使用服务令牌获取依赖

### 运行时核心
- GameRuntime 使用 PluginServiceRegistry
- 导出服务令牌相关类型

### 编辑器服务
- EngineService 适配新的服务令牌系统
- AssetRegistryService 优化

* fix: 修复 editor-app 和 behavior-tree-editor 中的类型引用

- editor-app/PluginLoader.ts: 使用 IPlugin 替代 IPluginLoader
- behavior-tree-editor: 使用 IEditorPlugin 替代 IPluginLoader

* fix(ui): 添加缺失的 ecs-engine-bindgen 依赖

UIRuntimeModule 使用 EngineBridgeToken,需要声明对 ecs-engine-bindgen 的依赖

* fix(type): 解决 ServiceToken 跨包类型兼容性问题

- 在 engine-core 中直接定义 ServiceToken 和 PluginServiceRegistry
  而不是从 plugin-types 重新导出,确保 tsup 生成的类型声明
  以 engine-core 作为类型来源
- 移除 RuntimeResolver.ts 中的硬编码模块 ID 检查,
  改用 module.json 中的 name 配置
- 修复 pnpm-lock.yaml 中的依赖记录

* refactor(arch): 改进架构设计,移除硬编码

- 统一类型导出:editor-core 从 engine-core 导入 IEditorModuleBase
- RuntimeResolver: 将硬编码路径改为配置常量和搜索路径列表
- 添加跨平台安装路径支持(Windows/macOS/Linux)
- 使用 ENGINE_WASM_CONFIG 配置引擎 WASM 文件信息
- IBundlePackOptions 添加 preloadBundles 配置项

* fix(particle): 添加缺失的 ecs-engine-bindgen 依赖

ParticleRuntimeModule 导入了 @esengine/ecs-engine-bindgen 的 tokens,
但 package.json 中未声明该依赖,导致 CI 构建失败。

* fix(physics-rapier2d): 移除不存在的 PhysicsSystemContext 导出

PhysicsRuntimeModule 中不存在该类型,导致 type-check 失败
2025-12-08 21:10:57 +08:00

228 lines
7.5 KiB
TypeScript

/**
* 世界流式加载编辑器模块
* World Streaming Editor Module
*
* Registers chunk visualizer, inspector providers and tools for world streaming.
*/
import type { ServiceContainer, Entity } from '@esengine/ecs-framework';
import { Core } from '@esengine/ecs-framework';
import type {
IEditorModuleLoader,
PanelDescriptor,
EntityCreationTemplate,
ComponentInspectorProviderDef,
IEditorPlugin,
ModuleManifest
} from '@esengine/editor-core';
import {
PanelPosition,
InspectorRegistry,
EntityStoreService,
MessageHub,
ComponentRegistry
} from '@esengine/editor-core';
import { TransformComponent } from '@esengine/engine-core';
import {
ChunkComponent,
StreamingAnchorComponent,
ChunkLoaderComponent,
WorldStreamingModule
} from '@esengine/world-streaming';
import { ChunkLoaderInspectorProvider } from './providers/ChunkLoaderInspectorProvider';
import { StreamingAnchorInspectorProvider } from './providers/StreamingAnchorInspectorProvider';
import './styles/ChunkVisualizer.css';
/**
* 世界流式加载编辑器模块
* World Streaming Editor Module
*/
export class WorldStreamingEditorModule implements IEditorModuleLoader {
async install(services: ServiceContainer): Promise<void> {
const inspectorRegistry = services.resolve(InspectorRegistry);
if (inspectorRegistry) {
inspectorRegistry.register(new ChunkLoaderInspectorProvider());
inspectorRegistry.register(new StreamingAnchorInspectorProvider());
}
const componentRegistry = services.resolve(ComponentRegistry);
if (componentRegistry) {
componentRegistry.register({
name: 'ChunkLoader',
type: ChunkLoaderComponent,
category: 'components.category.streaming',
description: 'Chunk-based world streaming controller',
icon: 'Grid3X3'
});
componentRegistry.register({
name: 'StreamingAnchor',
type: StreamingAnchorComponent,
category: 'components.category.streaming',
description: 'Streaming anchor point (player/camera)',
icon: 'Anchor'
});
componentRegistry.register({
name: 'Chunk',
type: ChunkComponent,
category: 'components.category.streaming',
description: 'Chunk entity marker',
icon: 'Square'
});
}
}
async uninstall(): Promise<void> {
// 清理 | Clean up
}
getPanels(): PanelDescriptor[] {
return [];
}
getInspectorProviders(): ComponentInspectorProviderDef[] {
return [
{
componentType: 'ChunkLoader',
priority: 100,
render: (component, entity, onChange) => {
const provider = new ChunkLoaderInspectorProvider();
return provider.render(
{ entityId: String(entity.id), component },
{ target: component, onChange }
);
}
},
{
componentType: 'StreamingAnchor',
priority: 100,
render: (component, entity, onChange) => {
const provider = new StreamingAnchorInspectorProvider();
return provider.render(
{ entityId: String(entity.id), component },
{ target: component, onChange }
);
}
}
];
}
getEntityCreationTemplates(): EntityCreationTemplate[] {
return [
{
id: 'create-streaming-anchor',
label: '创建流式锚点',
icon: 'Anchor',
category: 'streaming',
order: 100,
create: (): number => {
const scene = Core.scene;
if (!scene) {
throw new Error('Scene not available');
}
const entityStore = Core.services.resolve(EntityStoreService);
const messageHub = Core.services.resolve(MessageHub);
if (!entityStore || !messageHub) {
throw new Error('EntityStoreService or MessageHub not available');
}
const anchorCount = entityStore.getAllEntities()
.filter((e: Entity) => e.name.startsWith('StreamingAnchor ')).length;
const entityName = `StreamingAnchor ${anchorCount + 1}`;
const entity = scene.createEntity(entityName);
entity.addComponent(new TransformComponent());
entity.addComponent(new StreamingAnchorComponent());
entityStore.addEntity(entity);
messageHub.publish('entity:added', { entity });
messageHub.publish('scene:modified', {});
entityStore.selectEntity(entity);
return entity.id;
}
},
{
id: 'create-chunk-loader',
label: '创建区块加载器',
icon: 'Grid3X3',
category: 'streaming',
order: 101,
create: (): number => {
const scene = Core.scene;
if (!scene) {
throw new Error('Scene not available');
}
const entityStore = Core.services.resolve(EntityStoreService);
const messageHub = Core.services.resolve(MessageHub);
if (!entityStore || !messageHub) {
throw new Error('EntityStoreService or MessageHub not available');
}
const entity = scene.createEntity('ChunkLoader');
entity.addComponent(new ChunkLoaderComponent());
entityStore.addEntity(entity);
messageHub.publish('entity:added', { entity });
messageHub.publish('scene:modified', {});
entityStore.selectEntity(entity);
return entity.id;
}
}
];
}
getFileActionHandlers() {
return [];
}
getFileCreationTemplates() {
return [];
}
}
export const worldStreamingEditorModule = new WorldStreamingEditorModule();
/**
* 世界流式加载插件清单
* World Streaming Plugin Manifest
*/
const manifest: ModuleManifest = {
id: '@esengine/world-streaming',
name: '@esengine/world-streaming',
displayName: 'World Streaming',
version: '1.0.0',
description: 'Chunk-based world streaming for open world games',
category: 'Other',
isCore: false,
defaultEnabled: true,
isEngineModule: true,
canContainContent: false,
dependencies: ['engine-core'],
exports: {
components: ['ChunkComponent', 'StreamingAnchorComponent', 'ChunkLoaderComponent'],
systems: ['ChunkStreamingSystem', 'ChunkCullingSystem'],
other: ['ChunkManager']
}
};
/**
* 完整的世界流式加载插件(运行时 + 编辑器)
* Complete World Streaming Plugin (runtime + editor)
*/
export const WorldStreamingPlugin: IEditorPlugin = {
manifest,
runtimeModule: new WorldStreamingModule(),
editorModule: worldStreamingEditorModule
};
export default worldStreamingEditorModule;