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,239 @@
/**
* 通用资产收集器
* Generic Asset Collector
*
* 从序列化的场景数据中自动收集资产引用。
* 支持基于字段名模式和 Property 元数据两种识别方式。
*
* Automatically collects asset references from serialized scene data.
* Supports both field name pattern matching and Property metadata recognition.
*/
/**
* 场景资产引用信息(用于构建时收集)
* Scene asset reference info (for build-time collection)
*/
export interface SceneAssetRef {
/** 资产 GUID | Asset GUID */
guid: string;
/** 来源组件类型 | Source component type */
componentType: string;
/** 来源字段名 | Source field name */
fieldName: string;
/** 实体名称(可选)| Entity name (optional) */
entityName?: string;
}
/**
* 资产字段模式配置
* Asset field pattern configuration
*/
export interface AssetFieldPattern {
/** 字段名模式(正则表达式)| Field name pattern (regex) */
pattern: RegExp;
/** 字段类型(用于分类)| Field type (for categorization) */
type?: string;
}
/**
* 默认资产字段模式
* Default asset field patterns
*
* 这些模式用于识别常见的资产引用字段
* These patterns are used to identify common asset reference fields
*/
export const DEFAULT_ASSET_PATTERNS: AssetFieldPattern[] = [
// GUID 类字段 | GUID-like fields
{ pattern: /^.*[Gg]uid$/, type: 'guid' },
{ pattern: /^.*[Aa]sset[Ii]d$/, type: 'guid' },
{ pattern: /^.*[Aa]ssetGuid$/, type: 'guid' },
// 纹理/贴图字段 | Texture fields
{ pattern: /^texture$/, type: 'texture' },
{ pattern: /^.*[Tt]exture[Pp]ath$/, type: 'texture' },
// 音频字段 | Audio fields
{ pattern: /^clip$/, type: 'audio' },
{ pattern: /^.*[Aa]udio[Pp]ath$/, type: 'audio' },
// 通用路径字段 | Generic path fields
{ pattern: /^.*[Pp]ath$/, type: 'path' },
];
/**
* 检查值是否像 GUID
* Check if value looks like a GUID
*/
function isGuidLike(value: unknown): value is string {
if (typeof value !== 'string') return false;
// GUID 格式: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// 或者简单的包含连字符的长字符串
return /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value) ||
(value.includes('-') && value.length >= 30 && value.length <= 40);
}
/**
* 从组件数据中收集资产引用
* Collect asset references from component data
*/
function collectFromComponentData(
componentType: string,
data: Record<string, unknown>,
patterns: AssetFieldPattern[],
entityName?: string
): SceneAssetRef[] {
const references: SceneAssetRef[] = [];
for (const [fieldName, value] of Object.entries(data)) {
// 检查是否匹配任何资产字段模式
// Check if matches any asset field pattern
const matchesPattern = patterns.some(p => p.pattern.test(fieldName));
if (matchesPattern) {
// 处理单个值 | Handle single value
if (isGuidLike(value)) {
references.push({
guid: value,
componentType,
fieldName,
entityName
});
}
// 处理数组 | Handle array
else if (Array.isArray(value)) {
for (const item of value) {
if (isGuidLike(item)) {
references.push({
guid: item,
componentType,
fieldName,
entityName
});
}
}
}
}
// 特殊处理已知的数组字段(如 particleAssets
// Special handling for known array fields (like particleAssets)
if (fieldName === 'particleAssets' && Array.isArray(value)) {
for (const item of value) {
if (isGuidLike(item)) {
references.push({
guid: item,
componentType,
fieldName,
entityName
});
}
}
}
}
return references;
}
/**
* 实体类型定义(支持嵌套 children
* Entity type definition (supports nested children)
*/
interface EntityData {
name?: string;
components?: Array<{ type: string; data?: Record<string, unknown> }>;
children?: EntityData[];
}
/**
* 递归处理实体及其子实体
* Recursively process entity and its children
*/
function collectFromEntity(
entity: EntityData,
patterns: AssetFieldPattern[],
references: SceneAssetRef[]
): void {
const entityName = entity.name;
// 处理当前实体的组件 | Process current entity's components
if (entity.components) {
for (const component of entity.components) {
if (!component.data) continue;
const componentRefs = collectFromComponentData(
component.type,
component.data,
patterns,
entityName
);
references.push(...componentRefs);
}
}
// 递归处理子实体 | Recursively process children
if (entity.children && Array.isArray(entity.children)) {
for (const child of entity.children) {
collectFromEntity(child, patterns, references);
}
}
}
/**
* 从序列化的场景数据中收集所有资产引用
* Collect all asset references from serialized scene data
*
* @param sceneData 序列化的场景数据JSON 对象)| Serialized scene data (JSON object)
* @param patterns 资产字段模式(可选,默认使用内置模式)| Asset field patterns (optional, defaults to built-in patterns)
* @returns 资产引用列表 | List of asset references
*
* @example
* ```typescript
* const sceneData = JSON.parse(sceneJson);
* const references = collectAssetReferences(sceneData);
* for (const ref of references) {
* console.log(`Found asset ${ref.guid} in ${ref.componentType}.${ref.fieldName}`);
* }
* ```
*/
export function collectAssetReferences(
sceneData: { entities?: EntityData[] },
patterns: AssetFieldPattern[] = DEFAULT_ASSET_PATTERNS
): SceneAssetRef[] {
const references: SceneAssetRef[] = [];
if (!sceneData.entities) {
return references;
}
// 遍历顶层实体,递归处理嵌套的子实体
// Iterate top-level entities, recursively process nested children
for (const entity of sceneData.entities) {
collectFromEntity(entity, patterns, references);
}
return references;
}
/**
* 从资产引用列表中提取唯一的 GUID 集合
* Extract unique GUID set from asset references
*/
export function extractUniqueGuids(references: SceneAssetRef[]): Set<string> {
return new Set(references.map(ref => ref.guid));
}
/**
* 按组件类型分组资产引用
* Group asset references by component type
*/
export function groupByComponentType(references: SceneAssetRef[]): Map<string, SceneAssetRef[]> {
const groups = new Map<string, SceneAssetRef[]>();
for (const ref of references) {
const existing = groups.get(ref.componentType) || [];
existing.push(ref);
groups.set(ref.componentType, existing);
}
return groups;
}

View File

@@ -0,0 +1,79 @@
/**
* Asset Utilities
* 资产工具函数
*
* Provides common utilities for asset management:
* - GUID validation and generation (re-exported from core)
* - Content hashing
* 提供资产管理的通用工具:
* - GUID 验证和生成(从 core 重导出)
* - 内容哈希
*/
// Re-export GUID utilities from core (single source of truth)
// 从 core 重导出 GUID 工具(单一来源)
export { generateGUID, isValidGUID } from '@esengine/ecs-framework';
// ============================================================================
// Hash Utilities
// 哈希工具
// ============================================================================
/**
* Compute SHA-256 hash of an ArrayBuffer
* 计算 ArrayBuffer 的 SHA-256 哈希
*
* Returns first 16 hex characters of the hash.
* 返回哈希的前 16 个十六进制字符。
*
* @param buffer - The buffer to hash
* @returns Hash string (16 hex characters)
*/
export async function hashBuffer(buffer: ArrayBuffer): Promise<string> {
// Use Web Crypto API if available
// 如果可用则使用 Web Crypto API
if (typeof crypto !== 'undefined' && crypto.subtle) {
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
}
// Fallback: simple DJB2 hash
// 回退:简单的 DJB2 哈希
const view = new Uint8Array(buffer);
let hash = 5381;
for (let i = 0; i < view.length; i++) {
hash = ((hash << 5) + hash) ^ view[i];
}
return Math.abs(hash).toString(16).padStart(16, '0');
}
/**
* Compute hash of a string
* 计算字符串的哈希
*
* @param str - The string to hash
* @returns Hash string (8 hex characters)
*/
export function hashString(str: string): string {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) ^ str.charCodeAt(i);
}
return Math.abs(hash).toString(16).padStart(8, '0');
}
/**
* Compute content hash from file path and size
* 从文件路径和大小计算内容哈希
*
* This is a lightweight hash for quick comparison, not cryptographically secure.
* 这是一个用于快速比较的轻量级哈希,不具有加密安全性。
*
* @param path - File path
* @param size - File size in bytes
* @returns Hash string (8 hex characters)
*/
export function hashFileInfo(path: string, size: number): string {
return hashString(`${path}:${size}`);
}

View File

@@ -0,0 +1,227 @@
/**
* Path Validator
* 路径验证器
*
* Validates and sanitizes asset paths for security
* 验证并清理资产路径以确保安全
*/
/**
* Path validation options.
* 路径验证选项。
*/
export interface PathValidationOptions {
/** Allow absolute paths (for editor environment). | 允许绝对路径(用于编辑器环境)。 */
allowAbsolutePaths?: boolean;
/** Allow URLs (http://, https://, asset://). | 允许 URL。 */
allowUrls?: boolean;
}
export class PathValidator {
// Dangerous path patterns (without absolute path checks)
private static readonly DANGEROUS_PATTERNS_STRICT = [
/\.\.[/\\]/g, // Path traversal attempts (..)
/^[/\\]/, // Absolute paths on Unix
/^[a-zA-Z]:[/\\]/, // Absolute paths on Windows
/\0/, // Null bytes
/%00/, // URL encoded null bytes
/\.\.%2[fF]/ // URL encoded path traversal
];
// Dangerous path patterns (allowing absolute paths)
private static readonly DANGEROUS_PATTERNS_RELAXED = [
/\.\.[/\\]/g, // Path traversal attempts (..)
/\0/, // Null bytes
/%00/, // URL encoded null bytes
/\.\.%2[fF]/ // URL encoded path traversal
];
// Valid path characters for relative paths (alphanumeric, dash, underscore, dot, slash)
private static readonly VALID_PATH_REGEX = /^[a-zA-Z0-9\-_./\\@]+$/;
// Valid path characters for absolute paths (includes colon for Windows drives)
private static readonly VALID_ABSOLUTE_PATH_REGEX = /^[a-zA-Z0-9\-_./\\@:]+$/;
// URL pattern
private static readonly URL_REGEX = /^(https?|asset|blob|data):\/\//;
// Maximum path length
private static readonly MAX_PATH_LENGTH = 1024;
/** Global options for path validation. | 路径验证的全局选项。 */
private static _globalOptions: PathValidationOptions = {
allowAbsolutePaths: false,
allowUrls: true
};
/**
* Set global validation options.
* 设置全局验证选项。
*/
static setGlobalOptions(options: PathValidationOptions): void {
this._globalOptions = { ...this._globalOptions, ...options };
}
/**
* Get current global options.
* 获取当前全局选项。
*/
static getGlobalOptions(): PathValidationOptions {
return { ...this._globalOptions };
}
/**
* Validate if a path is safe
* 验证路径是否安全
*/
static validate(path: string, options?: PathValidationOptions): { valid: boolean; reason?: string } {
const opts = { ...this._globalOptions, ...options };
// Check for null/undefined/empty
if (!path || typeof path !== 'string') {
return { valid: false, reason: 'Path is empty or invalid' };
}
// Check length
if (path.length > this.MAX_PATH_LENGTH) {
return { valid: false, reason: `Path exceeds maximum length of ${this.MAX_PATH_LENGTH} characters` };
}
// Allow URLs if enabled
if (opts.allowUrls && this.URL_REGEX.test(path)) {
return { valid: true };
}
// Choose patterns based on options
const patterns = opts.allowAbsolutePaths
? this.DANGEROUS_PATTERNS_RELAXED
: this.DANGEROUS_PATTERNS_STRICT;
// Check for dangerous patterns
for (const pattern of patterns) {
if (pattern.test(path)) {
return { valid: false, reason: 'Path contains dangerous pattern' };
}
}
// Check for valid characters
const validCharsRegex = opts.allowAbsolutePaths
? this.VALID_ABSOLUTE_PATH_REGEX
: this.VALID_PATH_REGEX;
if (!validCharsRegex.test(path)) {
return { valid: false, reason: 'Path contains invalid characters' };
}
return { valid: true };
}
/**
* Sanitize a path
* 清理路径
*/
static sanitize(path: string): string {
if (!path || typeof path !== 'string') {
return '';
}
// Remove dangerous patterns
let sanitized = path;
// Remove path traversal (apply repeatedly until fully removed)
let prev;
do {
prev = sanitized;
sanitized = sanitized.replace(/\.\.[/\\]/g, '');
} while (sanitized !== prev);
// Remove leading slashes
sanitized = sanitized.replace(/^[/\\]+/, '');
// Remove null bytes
sanitized = sanitized.replace(/\0/g, '');
sanitized = sanitized.replace(/%00/g, '');
// Remove invalid Windows characters
sanitized = sanitized.replace(/[<>:"|?*]/g, '_');
// Normalize slashes
sanitized = sanitized.replace(/\\/g, '/');
// Remove double slashes
sanitized = sanitized.replace(/\/+/g, '/');
// Trim whitespace
sanitized = sanitized.trim();
// Truncate if too long
if (sanitized.length > this.MAX_PATH_LENGTH) {
sanitized = sanitized.substring(0, this.MAX_PATH_LENGTH);
}
return sanitized;
}
/**
* Check if path is trying to escape the base directory
* 检查路径是否试图逃离基础目录
*/
static isPathTraversal(path: string): boolean {
const normalized = path.replace(/\\/g, '/');
return normalized.includes('../') || normalized.includes('..\\');
}
/**
* Normalize a path for consistent handling
* 规范化路径以便一致处理
*/
static normalize(path: string): string {
if (!path) return '';
// Sanitize first
let normalized = this.sanitize(path);
// Convert backslashes to forward slashes
normalized = normalized.replace(/\\/g, '/');
// Remove trailing slash (except for root)
if (normalized.length > 1 && normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
/**
* Join path segments safely
* 安全地连接路径段
*/
static join(...segments: string[]): string {
const validSegments = segments
.filter((s) => s && typeof s === 'string')
.map((s) => this.sanitize(s))
.filter((s) => s.length > 0);
if (validSegments.length === 0) {
return '';
}
return this.normalize(validSegments.join('/'));
}
/**
* Get file extension safely
* 安全地获取文件扩展名
*/
static getExtension(path: string): string {
const sanitized = this.sanitize(path);
const lastDot = sanitized.lastIndexOf('.');
const lastSlash = sanitized.lastIndexOf('/');
if (lastDot > lastSlash && lastDot > 0) {
return sanitized.substring(lastDot + 1).toLowerCase();
}
return '';
}
}

View File

@@ -0,0 +1,81 @@
/**
* UV Coordinate Helper
* UV 坐标辅助工具
*
* 引擎使用图像坐标系:
* Engine uses image coordinate system:
* - 原点 (0, 0) 在左上角 | Origin at top-left
* - V 轴向下增长 | V-axis increases downward
* - UV 格式:[u0, v0, u1, v1] 其中 v0 < v1
*/
export class UVHelper {
/**
* Calculate UV coordinates for a texture region
* 计算纹理区域的 UV 坐标
*/
static calculateUV(
imageRect: { x: number; y: number; width: number; height: number },
textureSize: { width: number; height: number }
): [number, number, number, number] {
const { x, y, width, height } = imageRect;
const { width: tw, height: th } = textureSize;
return [
x / tw, // u0
y / th, // v0
(x + width) / tw, // u1
(y + height) / th // v1
];
}
/**
* Calculate UV coordinates for a tile in a tileset
* 计算 tileset 中某个 tile 的 UV 坐标
*/
static calculateTileUV(
tileIndex: number,
tilesetInfo: {
columns: number;
tileWidth: number;
tileHeight: number;
imageWidth: number;
imageHeight: number;
margin?: number;
spacing?: number;
}
): [number, number, number, number] | null {
if (tileIndex < 0) return null;
const {
columns,
tileWidth,
tileHeight,
imageWidth,
imageHeight,
margin = 0,
spacing = 0
} = tilesetInfo;
const col = tileIndex % columns;
const row = Math.floor(tileIndex / columns);
const x = margin + col * (tileWidth + spacing);
const y = margin + row * (tileHeight + spacing);
return this.calculateUV(
{ x, y, width: tileWidth, height: tileHeight },
{ width: imageWidth, height: imageHeight }
);
}
static validateUV(uv: [number, number, number, number]): boolean {
const [u0, v0, u1, v1] = uv;
return u0 >= 0 && u0 <= 1 && u1 >= 0 && u1 <= 1 &&
v0 >= 0 && v0 <= 1 && v1 >= 0 && v1 <= 1 &&
u0 < u1 && v0 < v1;
}
static debugPrint(uv: [number, number, number, number], label?: string): void {
const prefix = label ? `[${label}] ` : '';
console.log(`${prefix}UV: [${uv.map(n => n.toFixed(4)).join(', ')}]`);
}
}