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

View File

@@ -0,0 +1,139 @@
/**
* Asset Metadata Service
* 资产元数据服务
*
* Provides global access to asset metadata without requiring asset loading.
* This service is independent of the texture loading path, allowing
* render systems to query sprite info regardless of how textures are loaded.
*
* 提供对资产元数据的全局访问,无需加载资产。
* 此服务独立于纹理加载路径,允许渲染系统查询 sprite 信息,
* 无论纹理是如何加载的。
*/
import { AssetDatabase, ITextureSpriteInfo } from '../core/AssetDatabase';
import type { AssetGUID } from '../types/AssetTypes';
import type { ITextureEngineBridge } from '../integration/EngineIntegration';
/**
* Global asset database instance
* 全局资产数据库实例
*/
let globalAssetDatabase: AssetDatabase | null = null;
/**
* Global engine bridge instance
* 全局引擎桥实例
*
* Used to query texture dimensions from Rust engine (single source of truth).
* 用于从 Rust 引擎查询纹理尺寸(唯一事实来源)。
*/
let globalEngineBridge: ITextureEngineBridge | null = null;
/**
* Set the global asset database
* 设置全局资产数据库
*
* Should be called during engine initialization.
* 应在引擎初始化期间调用。
*
* @param database - AssetDatabase instance | AssetDatabase 实例
*/
export function setGlobalAssetDatabase(database: AssetDatabase | null): void {
globalAssetDatabase = database;
}
/**
* Get the global asset database
* 获取全局资产数据库
*
* @returns AssetDatabase instance or null | AssetDatabase 实例或 null
*/
export function getGlobalAssetDatabase(): AssetDatabase | null {
return globalAssetDatabase;
}
/**
* Set the global engine bridge
* 设置全局引擎桥
*
* The engine bridge is used to query texture dimensions directly from Rust engine.
* This is the single source of truth for texture dimensions.
* 引擎桥用于直接从 Rust 引擎查询纹理尺寸。
* 这是纹理尺寸的唯一事实来源。
*
* @param bridge - ITextureEngineBridge instance | ITextureEngineBridge 实例
*/
export function setGlobalEngineBridge(bridge: ITextureEngineBridge | null): void {
globalEngineBridge = bridge;
}
/**
* Get the global engine bridge
* 获取全局引擎桥
*
* @returns ITextureEngineBridge instance or null | ITextureEngineBridge 实例或 null
*/
export function getGlobalEngineBridge(): ITextureEngineBridge | null {
return globalEngineBridge;
}
/**
* Get texture sprite info by GUID
* 通过 GUID 获取纹理 Sprite 信息
*
* This is the primary API for render systems to query nine-patch/sprite info.
* It combines data from:
* - Asset metadata (sliceBorder, pivot) from AssetDatabase
* - Texture dimensions (width, height) from Rust engine (single source of truth)
*
* 这是渲染系统查询九宫格/sprite 信息的主要 API。
* 它合并来自:
* - AssetDatabase 的资产元数据sliceBorder, pivot
* - Rust 引擎的纹理尺寸width, height唯一事实来源
*
* @param guid - Texture asset GUID | 纹理资产 GUID
* @returns Sprite info or undefined | Sprite 信息或 undefined
*/
export function getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
// Get sprite settings from metadata
// 从元数据获取 sprite 设置
const metadataInfo = globalAssetDatabase?.getTextureSpriteInfo(guid);
// Get texture dimensions from Rust engine (single source of truth)
// 从 Rust 引擎获取纹理尺寸(唯一事实来源)
let dimensions: { width: number; height: number } | undefined;
if (globalEngineBridge?.getTextureInfoByPath && globalAssetDatabase) {
// Get asset path from database
// 从数据库获取资产路径
const metadata = globalAssetDatabase.getMetadata(guid);
if (metadata?.path) {
const engineInfo = globalEngineBridge.getTextureInfoByPath(metadata.path);
if (engineInfo) {
dimensions = engineInfo;
}
}
}
// If no metadata and no dimensions, return undefined
// 如果没有元数据也没有尺寸,返回 undefined
if (!metadataInfo && !dimensions) {
return undefined;
}
// Merge the two sources
// 合并两个数据源
// Prefer engine dimensions (runtime loaded), fallback to metadata dimensions (catalog stored)
// 优先使用引擎尺寸(运行时加载),后备使用元数据尺寸(目录存储)
return {
sliceBorder: metadataInfo?.sliceBorder,
pivot: metadataInfo?.pivot,
width: dimensions?.width ?? metadataInfo?.width,
height: dimensions?.height ?? metadataInfo?.height
};
}
// Re-export type for convenience
// 为方便起见重新导出类型
export type { ITextureSpriteInfo };

View File

@@ -0,0 +1,239 @@
/**
* 路径解析服务
* Path Resolution Service
*
* 提供统一的路径解析接口处理编辑器、Catalog、运行时三层路径转换。
* Provides unified path resolution interface for editor, catalog, and runtime path conversion.
*
* 路径格式约定 | Path Format Convention:
* - 编辑器路径 (Editor Path): 绝对路径,如 `C:\Project\assets\textures\bg.png`
* - Catalog 路径 (Catalog Path): 相对于 assets 目录,不含 `assets/` 前缀,如 `textures/bg.png`
* - 运行时 URL (Runtime URL): 完整 URL如 `./assets/textures/bg.png` 或 `https://cdn.example.com/assets/textures/bg.png`
*
* @example
* ```typescript
* import { PathResolutionServiceToken, type IPathResolutionService } from '@esengine/asset-system';
*
* // 获取服务
* const pathService = context.services.get(PathResolutionServiceToken);
*
* // Catalog 路径转运行时 URL
* const url = pathService.catalogToRuntime('textures/bg.png');
* // => './assets/textures/bg.png'
*
* // 编辑器路径转 Catalog 路径
* const catalogPath = pathService.editorToCatalog('C:\\Project\\assets\\textures\\bg.png', 'C:\\Project');
* // => 'textures/bg.png'
* ```
*/
import { createServiceToken } from '@esengine/ecs-framework';
// ============================================================================
// 接口定义 | Interface Definitions
// ============================================================================
/**
* 路径解析服务接口
* Path resolution service interface
*/
export interface IPathResolutionService {
/**
* 将 Catalog 路径转换为运行时 URL
* Convert catalog path to runtime URL
*
* @param catalogPath Catalog 路径(相对于 assets 目录,不含 assets/ 前缀)
* @returns 运行时 URL
*
* @example
* ```typescript
* // 输入: 'textures/bg.png'
* // 输出: './assets/textures/bg.png' (取决于 baseUrl 配置)
* pathService.catalogToRuntime('textures/bg.png');
* ```
*/
catalogToRuntime(catalogPath: string): string;
/**
* 将编辑器绝对路径转换为 Catalog 路径
* Convert editor absolute path to catalog path
*
* @param editorPath 编辑器绝对路径
* @param projectRoot 项目根目录
* @returns Catalog 路径(相对于 assets 目录,不含 assets/ 前缀)
*
* @example
* ```typescript
* // 输入: 'C:\\Project\\assets\\textures\\bg.png', 'C:\\Project'
* // 输出: 'textures/bg.png'
* pathService.editorToCatalog('C:\\Project\\assets\\textures\\bg.png', 'C:\\Project');
* ```
*/
editorToCatalog(editorPath: string, projectRoot: string): string;
/**
* 设置运行时基础 URL
* Set runtime base URL
*
* @param url 基础 URL通常为 './assets' 或 CDN URL
*/
setBaseUrl(url: string): void;
/**
* 获取当前基础 URL
* Get current base URL
*/
getBaseUrl(): string;
/**
* 规范化路径(统一斜杠方向,移除重复斜杠)
* Normalize path (unify slash direction, remove duplicate slashes)
*
* @param path 输入路径
* @returns 规范化后的路径
*/
normalize(path: string): string;
/**
* 检查路径是否为绝对 URL
* Check if path is absolute URL
*
* @param path 输入路径
* @returns 是否为绝对 URL
*/
isAbsoluteUrl(path: string): boolean;
}
// ============================================================================
// 服务令牌 | Service Token
// ============================================================================
/**
* 路径解析服务令牌
* Path resolution service token
*/
export const PathResolutionServiceToken = createServiceToken<IPathResolutionService>('pathResolutionService');
// ============================================================================
// 默认实现 | Default Implementation
// ============================================================================
/**
* 路径解析服务默认实现
* Default path resolution service implementation
*/
export class PathResolutionService implements IPathResolutionService {
private _baseUrl: string = './assets';
private _assetsDir: string = 'assets';
/**
* 创建路径解析服务
* Create path resolution service
*
* @param baseUrl 基础 URL默认 './assets'
*/
constructor(baseUrl?: string) {
if (baseUrl !== undefined) {
this._baseUrl = baseUrl;
}
}
/**
* 将 Catalog 路径转换为运行时 URL
* Convert catalog path to runtime URL
*/
catalogToRuntime(catalogPath: string): string {
// 空路径直接返回
if (!catalogPath) {
return catalogPath;
}
// 已经是绝对 URL 则直接返回
if (this.isAbsoluteUrl(catalogPath)) {
return catalogPath;
}
// Data URL 直接返回
if (catalogPath.startsWith('data:')) {
return catalogPath;
}
// 规范化路径
let normalized = this.normalize(catalogPath);
// 移除开头的斜杠
normalized = normalized.replace(/^\/+/, '');
// 如果路径以 'assets/' 开头,移除它(避免重复)
// Catalog 路径不应包含 assets/ 前缀
if (normalized.startsWith('assets/')) {
normalized = normalized.substring(7);
}
// 构建完整 URL
const base = this._baseUrl.replace(/\/+$/, ''); // 移除尾部斜杠
return `${base}/${normalized}`;
}
/**
* 将编辑器绝对路径转换为 Catalog 路径
* Convert editor absolute path to catalog path
*/
editorToCatalog(editorPath: string, projectRoot: string): string {
// 规范化路径
let normalizedPath = this.normalize(editorPath);
let normalizedRoot = this.normalize(projectRoot);
// 确保根路径以斜杠结尾
if (!normalizedRoot.endsWith('/')) {
normalizedRoot += '/';
}
// 移除项目根路径前缀
if (normalizedPath.startsWith(normalizedRoot)) {
normalizedPath = normalizedPath.substring(normalizedRoot.length);
}
// 移除 assets/ 前缀(如果存在)
const assetsPrefix = `${this._assetsDir}/`;
if (normalizedPath.startsWith(assetsPrefix)) {
normalizedPath = normalizedPath.substring(assetsPrefix.length);
}
return normalizedPath;
}
/**
* 设置运行时基础 URL
* Set runtime base URL
*/
setBaseUrl(url: string): void {
this._baseUrl = url;
}
/**
* 获取当前基础 URL
* Get current base URL
*/
getBaseUrl(): string {
return this._baseUrl;
}
/**
* 规范化路径
* Normalize path
*/
normalize(path: string): string {
return path
.replace(/\\/g, '/') // 反斜杠转正斜杠
.replace(/\/+/g, '/'); // 移除重复斜杠
}
/**
* 检查路径是否为绝对 URL
* Check if path is absolute URL
*/
isAbsoluteUrl(path: string): boolean {
return /^(https?:\/\/|file:\/\/|asset:\/\/|blob:)/.test(path);
}
}

View File

@@ -0,0 +1,358 @@
/**
* 场景资源管理器 - 集中式场景资源加载
* SceneResourceManager - Centralized resource loading for scenes
*
* 扫描场景中所有组件,收集资源引用,批量加载资源,并将运行时 ID 分配回组件
* Scans all components in a scene, collects resource references, batch-loads them, and assigns runtime IDs back to components
*/
import type { Scene } from '@esengine/ecs-framework';
import { isResourceComponent, type ResourceReference } from '../interfaces/IResourceComponent';
/**
* 资源加载器接口
* Resource loader interface
*/
export interface IResourceLoader {
/**
* 批量加载资源并返回路径到 ID 的映射
* Load a batch of resources and return path-to-ID mapping
* @param paths 资源路径数组 / Array of resource paths
* @param type 资源类型 / Resource type
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
*/
loadResourcesBatch(paths: string[], type: ResourceReference['type']): Promise<Map<string, number>>;
/**
* 卸载纹理资源(可选)
* Unload texture resource (optional)
*/
unloadTexture?(textureId: number): void;
/**
* 卸载音频资源(可选)
* Unload audio resource (optional)
*/
unloadAudio?(audioId: number): void;
/**
* 卸载数据资源(可选)
* Unload data resource (optional)
*/
unloadData?(dataId: number): void;
}
/**
* 资源引用计数条目
* Resource reference count entry
*/
interface ResourceRefCountEntry {
/** 资源路径 / Resource path */
path: string;
/** 资源类型 / Resource type */
type: ResourceReference['type'];
/** 运行时 ID / Runtime ID */
runtimeId: number;
/** 使用此资源的场景名称集合 / Set of scene names using this resource */
sceneNames: Set<string>;
}
export class SceneResourceManager {
private resourceLoader: IResourceLoader | null = null;
/**
* 资源引用计数表
* Resource reference count table
*
* Key: resource path, Value: reference count entry
*/
private _resourceRefCounts = new Map<string, ResourceRefCountEntry>();
/**
* 场景到其使用的资源路径的映射
* Map of scene name to resource paths used by that scene
*/
private _sceneResources = new Map<string, Set<string>>();
/**
* 设置资源加载器实现
* Set the resource loader implementation
*
* 应由引擎集成层调用
* This should be called by the engine integration layer
*
* @param loader 资源加载器实例 / Resource loader instance
*/
setResourceLoader(loader: IResourceLoader): void {
this.resourceLoader = loader;
}
/**
* 加载场景所需的所有资源
* Load all resources required by a scene
*
* 流程 / Process:
* 1. 扫描所有实体并从 IResourceComponent 实现中收集资源引用
* Scan all entities and collect resource references from IResourceComponent implementations
* 2. 按类型分组资源(纹理、音频等)
* Group resources by type (texture, audio, etc.)
* 3. 批量加载每种资源类型
* Batch load each resource type
* 4. 将运行时 ID 分配回组件
* Assign runtime IDs back to components
* 5. 更新引用计数
* Update reference counts
*
* @param scene 要加载资源的场景 / The scene to load resources for
* @returns 当所有资源加载完成时解析的 Promise / Promise that resolves when all resources are loaded
*/
async loadSceneResources(scene: Scene): Promise<void> {
if (!this.resourceLoader) {
console.warn('[SceneResourceManager] No resource loader set, skipping resource loading');
return;
}
const sceneName = scene.name;
// 从组件收集所有资源引用 / Collect all resource references from components
const resourceRefs = this.collectResourceReferences(scene);
if (resourceRefs.length === 0) {
return;
}
// 按资源类型分组 / Group by resource type
const resourcesByType = new Map<ResourceReference['type'], Set<string>>();
for (const ref of resourceRefs) {
if (!resourcesByType.has(ref.type)) {
resourcesByType.set(ref.type, new Set());
}
resourcesByType.get(ref.type)!.add(ref.path);
}
// 批量加载每种资源类型 / Load each resource type in batch
const allResourceIds = new Map<string, number>();
for (const [type, paths] of resourcesByType) {
const pathsArray = Array.from(paths);
try {
const resourceIds = await this.resourceLoader.loadResourcesBatch(pathsArray, type);
// 合并到总映射表 / Merge into combined map
for (const [path, id] of resourceIds) {
allResourceIds.set(path, id);
// 更新引用计数 / Update reference count
this.addResourceReference(path, type, id, sceneName);
}
} catch (error) {
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
}
}
// 将资源 ID 分配回组件 / Assign resource IDs back to components
this.assignResourceIds(scene, allResourceIds);
// 记录场景使用的资源 / Record resources used by scene
const scenePaths = new Set<string>();
for (const ref of resourceRefs) {
scenePaths.add(ref.path);
}
this._sceneResources.set(sceneName, scenePaths);
}
/**
* 添加资源引用
* Add resource reference
*/
private addResourceReference(
path: string,
type: ResourceReference['type'],
runtimeId: number,
sceneName: string
): void {
let entry = this._resourceRefCounts.get(path);
if (!entry) {
entry = {
path,
type,
runtimeId,
sceneNames: new Set()
};
this._resourceRefCounts.set(path, entry);
}
entry.sceneNames.add(sceneName);
}
/**
* 移除资源引用
* Remove resource reference
*
* @returns true 如果资源引用计数归零 / true if resource reference count reaches zero
*/
private removeResourceReference(path: string, sceneName: string): boolean {
const entry = this._resourceRefCounts.get(path);
if (!entry) {
return false;
}
entry.sceneNames.delete(sceneName);
if (entry.sceneNames.size === 0) {
this._resourceRefCounts.delete(path);
return true;
}
return false;
}
/**
* 从场景实体收集所有资源引用
* Collect all resource references from scene entities
*/
private collectResourceReferences(scene: Scene): ResourceReference[] {
const refs: ResourceReference[] = [];
for (const entity of scene.entities.buffer) {
for (const component of entity.components) {
if (isResourceComponent(component)) {
const componentRefs = component.getResourceReferences();
refs.push(...componentRefs);
}
}
}
return refs;
}
/**
* 将已加载的资源 ID 分配回组件
* Assign loaded resource IDs back to components
*
* @param scene 场景 / Scene
* @param pathToId 路径到 ID 的映射 / Path to ID mapping
*/
private assignResourceIds(scene: Scene, pathToId: Map<string, number>): void {
for (const entity of scene.entities.buffer) {
for (const component of entity.components) {
if (isResourceComponent(component)) {
component.setResourceIds(pathToId);
}
}
}
}
/**
* 卸载场景使用的所有资源
* Unload all resources used by a scene
*
* 在场景销毁时调用,只会卸载不再被其他场景引用的资源
* Called when a scene is being destroyed, only unloads resources not referenced by other scenes
*
* @param scene 要卸载资源的场景 / The scene to unload resources for
*/
async unloadSceneResources(scene: Scene): Promise<void> {
const sceneName = scene.name;
// 获取场景使用的资源路径 / Get resource paths used by scene
const scenePaths = this._sceneResources.get(sceneName);
if (!scenePaths) {
return;
}
// 要卸载的资源 / Resources to unload
const toUnload: ResourceRefCountEntry[] = [];
// 移除引用并收集需要卸载的资源 / Remove references and collect resources to unload
for (const path of scenePaths) {
const entry = this._resourceRefCounts.get(path);
if (entry) {
const shouldUnload = this.removeResourceReference(path, sceneName);
if (shouldUnload) {
toUnload.push(entry);
}
}
}
// 清理场景资源记录 / Clean up scene resource record
this._sceneResources.delete(sceneName);
// 卸载不再使用的资源 / Unload resources no longer in use
if (this.resourceLoader && toUnload.length > 0) {
for (const entry of toUnload) {
this.unloadResource(entry);
}
}
}
/**
* 卸载单个资源
* Unload a single resource
*/
private unloadResource(entry: ResourceRefCountEntry): void {
if (!this.resourceLoader) return;
switch (entry.type) {
case 'texture':
if (this.resourceLoader.unloadTexture) {
this.resourceLoader.unloadTexture(entry.runtimeId);
}
break;
case 'audio':
if (this.resourceLoader.unloadAudio) {
this.resourceLoader.unloadAudio(entry.runtimeId);
}
break;
case 'data':
if (this.resourceLoader.unloadData) {
this.resourceLoader.unloadData(entry.runtimeId);
}
break;
case 'font':
// 字体卸载暂未实现 / Font unloading not yet implemented
break;
}
}
/**
* 获取资源统计信息
* Get resource statistics
*/
getStatistics(): {
totalResources: number;
trackedScenes: number;
resourcesByType: Map<ResourceReference['type'], number>;
} {
const resourcesByType = new Map<ResourceReference['type'], number>();
for (const entry of this._resourceRefCounts.values()) {
const count = resourcesByType.get(entry.type) || 0;
resourcesByType.set(entry.type, count + 1);
}
return {
totalResources: this._resourceRefCounts.size,
trackedScenes: this._sceneResources.size,
resourcesByType
};
}
/**
* 获取资源的引用计数
* Get reference count for a resource
*/
getResourceRefCount(path: string): number {
const entry = this._resourceRefCounts.get(path);
return entry ? entry.sceneNames.size : 0;
}
/**
* 清空所有跟踪数据
* Clear all tracking data
*/
clearAll(): void {
this._resourceRefCounts.clear();
this._sceneResources.clear();
}
}