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,142 @@
import { createLogger } from '@esengine/ecs-framework';
const logger = createLogger('EditorEventBus');
type EventHandler<T = any> = (data: T) => void;
interface Subscription {
unsubscribe: () => void;
}
export enum EditorEvent {
NODE_CREATED = 'node:created',
NODE_DELETED = 'node:deleted',
NODE_UPDATED = 'node:updated',
NODE_MOVED = 'node:moved',
NODE_SELECTED = 'node:selected',
CONNECTION_ADDED = 'connection:added',
CONNECTION_REMOVED = 'connection:removed',
EXECUTION_STARTED = 'execution:started',
EXECUTION_PAUSED = 'execution:paused',
EXECUTION_RESUMED = 'execution:resumed',
EXECUTION_STOPPED = 'execution:stopped',
EXECUTION_STEPPED = 'execution:stepped',
EXECUTION_TICK = 'execution:tick',
EXECUTION_NODE_STATUS_CHANGED = 'execution:node_status_changed',
TREE_SAVED = 'tree:saved',
TREE_LOADED = 'tree:loaded',
TREE_VALIDATED = 'tree:validated',
BLACKBOARD_VARIABLE_UPDATED = 'blackboard:variable_updated',
BLACKBOARD_RESTORED = 'blackboard:restored',
CANVAS_ZOOM_CHANGED = 'canvas:zoom_changed',
CANVAS_PAN_CHANGED = 'canvas:pan_changed',
CANVAS_RESET = 'canvas:reset',
COMMAND_EXECUTED = 'command:executed',
COMMAND_UNDONE = 'command:undone',
COMMAND_REDONE = 'command:redone'
}
export class EditorEventBus {
private listeners: Map<string, Set<EventHandler>> = new Map();
private eventHistory: Array<{ event: string; data: any; timestamp: number }> = [];
private maxHistorySize: number = 100;
on<T = any>(event: EditorEvent | string, handler: EventHandler<T>): Subscription {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
return {
unsubscribe: () => this.off(event, handler)
};
}
once<T = any>(event: EditorEvent | string, handler: EventHandler<T>): Subscription {
const wrappedHandler = (data: T) => {
handler(data);
this.off(event, wrappedHandler);
};
return this.on(event, wrappedHandler);
}
off<T = any>(event: EditorEvent | string, handler: EventHandler<T>): void {
const handlers = this.listeners.get(event);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.listeners.delete(event);
}
}
}
emit<T = any>(event: EditorEvent | string, data?: T): void {
if (this.eventHistory.length >= this.maxHistorySize) {
this.eventHistory.shift();
}
this.eventHistory.push({
event,
data,
timestamp: Date.now()
});
const handlers = this.listeners.get(event);
if (handlers) {
handlers.forEach((handler) => {
try {
handler(data);
} catch (error) {
logger.error(`Error in event handler for ${event}:`, error);
}
});
}
}
clear(event?: EditorEvent | string): void {
if (event) {
this.listeners.delete(event);
} else {
this.listeners.clear();
}
}
getListenerCount(event: EditorEvent | string): number {
return this.listeners.get(event)?.size || 0;
}
getAllEvents(): string[] {
return Array.from(this.listeners.keys());
}
getEventHistory(count?: number): Array<{ event: string; data: any; timestamp: number }> {
if (count) {
return this.eventHistory.slice(-count);
}
return [...this.eventHistory];
}
clearHistory(): void {
this.eventHistory = [];
}
}
let globalEventBus: EditorEventBus | null = null;
export function getGlobalEventBus(): EditorEventBus {
if (!globalEventBus) {
globalEventBus = new EditorEventBus();
}
return globalEventBus;
}
export function resetGlobalEventBus(): void {
globalEventBus = null;
}

View File

@@ -0,0 +1,97 @@
import { NodeTemplates, type NodeTemplate } from '@esengine/behavior-tree';
import { Node } from '../../domain/models/Node';
import { Position } from '../../domain/value-objects/Position';
import { INodeFactory } from '../../domain/interfaces/INodeFactory';
import { NodeRegistryService } from '../services/NodeRegistryService';
/**
* 生成唯一ID
*/
function generateUniqueId(): string {
return `node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 节点工厂实现
*/
export class NodeFactory implements INodeFactory {
/**
* 创建节点
*/
createNode(
template: NodeTemplate,
position: Position,
data?: Record<string, unknown>
): Node {
const nodeId = generateUniqueId();
const nodeData = {
...template.defaultConfig,
...data
};
return new Node(nodeId, template, nodeData, position, []);
}
/**
* 根据模板类型创建节点
*/
createNodeByType(
nodeType: string,
position: Position,
data?: Record<string, unknown>
): Node {
const template = this.getTemplateByType(nodeType);
if (!template) {
throw new Error(`未找到节点模板: ${nodeType}`);
}
return this.createNode(template, position, data);
}
/**
* 克隆节点
*/
cloneNode(node: Node, newPosition?: Position): Node {
const position = newPosition || node.position;
const clonedId = generateUniqueId();
return new Node(
clonedId,
node.template,
node.data,
position,
[]
);
}
/**
* 获取所有可用的节点模板
*/
getAllTemplates(): NodeTemplate[] {
const coreTemplates = NodeTemplates.getAllTemplates();
const customTemplates = NodeRegistryService.getInstance().getCustomTemplates();
return [...coreTemplates, ...customTemplates];
}
/**
* 根据类型获取模板
*/
private getTemplateByType(nodeType: string): NodeTemplate | null {
const allTemplates = this.getAllTemplates();
const template = allTemplates.find((t: NodeTemplate) => {
const defaultNodeType = t.defaultConfig.nodeType;
return defaultNodeType === nodeType;
});
return template || null;
}
/**
* 根据实现类型获取模板
*/
getTemplateByImplementationType(implementationType: string): NodeTemplate | null {
const allTemplates = this.getAllTemplates();
return allTemplates.find((t) => t.className === implementationType) || null;
}
}

View File

@@ -0,0 +1 @@
export { NodeFactory } from './NodeFactory';

View File

@@ -0,0 +1,2 @@
export * from './factories';
export * from './serialization';

View File

@@ -0,0 +1,127 @@
import { BehaviorTree } from '../../domain/models/BehaviorTree';
import { ISerializer, SerializationFormat } from '../../domain/interfaces/ISerializer';
import { BehaviorTreeAssetSerializer, EditorFormatConverter } from '@esengine/behavior-tree';
/**
* 序列化选项
*/
export interface SerializationOptions {
/**
* 资产版本号
*/
version?: string;
/**
* 资产名称
*/
name?: string;
/**
* 资产描述
*/
description?: string;
/**
* 创建时间
*/
createdAt?: string;
/**
* 修改时间
*/
modifiedAt?: string;
}
/**
* 行为树序列化器实现
*/
export class BehaviorTreeSerializer implements ISerializer {
private readonly defaultOptions: Required<SerializationOptions> = {
version: '1.0.0',
name: 'Untitled Behavior Tree',
description: '',
createdAt: new Date().toISOString(),
modifiedAt: new Date().toISOString()
};
constructor(private readonly options: SerializationOptions = {}) {
this.defaultOptions = { ...this.defaultOptions, ...options };
}
/**
* 序列化行为树
*/
serialize(tree: BehaviorTree, format: SerializationFormat): string | Uint8Array {
const treeObject = tree.toObject();
if (format === 'json') {
return JSON.stringify(treeObject, null, 2);
}
throw new Error(`不支持的序列化格式: ${format}`);
}
/**
* 反序列化行为树
*/
deserialize(data: string | Uint8Array, format: SerializationFormat): BehaviorTree {
if (format === 'json') {
if (typeof data !== 'string') {
throw new Error('JSON 格式需要字符串数据');
}
const obj = JSON.parse(data);
return BehaviorTree.fromObject(obj);
}
throw new Error(`不支持的反序列化格式: ${format}`);
}
/**
* 导出为运行时资产格式
* @param tree 行为树
* @param format 导出格式
* @param options 可选的序列化选项(覆盖默认值)
*/
exportToRuntimeAsset(
tree: BehaviorTree,
format: SerializationFormat,
options?: SerializationOptions
): string | Uint8Array {
const nodes = tree.nodes.map((node) => ({
id: node.id,
template: node.template,
data: node.data,
position: node.position.toObject(),
children: Array.from(node.children)
}));
const connections = tree.connections.map((conn) => conn.toObject());
const blackboard = tree.blackboard.toObject();
const finalOptions = { ...this.defaultOptions, ...options };
finalOptions.modifiedAt = new Date().toISOString();
const editorFormat = {
version: finalOptions.version,
metadata: {
name: finalOptions.name,
description: finalOptions.description,
createdAt: finalOptions.createdAt,
modifiedAt: finalOptions.modifiedAt
},
nodes,
connections,
blackboard
};
const asset = EditorFormatConverter.toAsset(editorFormat);
if (format === 'json') {
return BehaviorTreeAssetSerializer.serialize(asset, { format: 'json', pretty: true });
} else if (format === 'binary') {
return BehaviorTreeAssetSerializer.serialize(asset, { format: 'binary' });
}
throw new Error(`不支持的导出格式: ${format}`);
}
}

View File

@@ -0,0 +1 @@
export { BehaviorTreeSerializer } from './BehaviorTreeSerializer';

View File

@@ -0,0 +1,264 @@
import { NodeMetadataRegistry, NodeType, type NodeTemplate, type NodeMetadata } from '@esengine/behavior-tree';
/**
* 简化的节点注册配置
*/
export interface NodeRegistrationConfig {
type: 'composite' | 'decorator' | 'action' | 'condition';
implementationType: string;
displayName: string;
description?: string;
category?: string;
icon?: string;
color?: string;
properties?: NodePropertyConfig[];
minChildren?: number;
maxChildren?: number;
}
/**
* 节点属性配置
*/
export interface NodePropertyConfig {
name: string;
type: 'string' | 'number' | 'boolean' | 'select' | 'blackboard' | 'code';
label: string;
description?: string;
defaultValue?: any;
options?: Array<{ label: string; value: any }>;
min?: number;
max?: number;
required?: boolean;
}
/**
* 节点注册服务
* 提供编辑器级别的节点注册和管理功能
*/
export class NodeRegistryService {
private static _instance: NodeRegistryService;
private _customTemplates: Map<string, NodeTemplate> = new Map();
private _registrationCallbacks: Array<(template: NodeTemplate) => void> = [];
private constructor() {}
static getInstance(): NodeRegistryService {
if (!this._instance) {
this._instance = new NodeRegistryService();
}
return this._instance;
}
/**
* 注册自定义节点类型
*/
registerNode(config: NodeRegistrationConfig): void {
const nodeType = this._mapStringToNodeType(config.type);
const metadata: NodeMetadata = {
implementationType: config.implementationType,
nodeType: nodeType,
displayName: config.displayName,
description: config.description || '',
category: config.category || this._getDefaultCategory(config.type),
configSchema: this._convertPropertiesToSchema(config.properties || []),
childrenConstraints: this._getChildrenConstraints(config)
};
class DummyExecutor {}
NodeMetadataRegistry.register(DummyExecutor, metadata);
const template = this._createTemplate(config, metadata);
this._customTemplates.set(config.implementationType, template);
this._registrationCallbacks.forEach((cb) => cb(template));
}
/**
* 注销节点类型
*/
unregisterNode(implementationType: string): boolean {
return this._customTemplates.delete(implementationType);
}
/**
* 获取所有自定义模板
*/
getCustomTemplates(): NodeTemplate[] {
return Array.from(this._customTemplates.values());
}
/**
* 检查节点类型是否已注册
*/
hasNode(implementationType: string): boolean {
return this._customTemplates.has(implementationType) ||
NodeMetadataRegistry.getMetadata(implementationType) !== undefined;
}
/**
* 监听节点注册事件
*/
onNodeRegistered(callback: (template: NodeTemplate) => void): () => void {
this._registrationCallbacks.push(callback);
return () => {
const index = this._registrationCallbacks.indexOf(callback);
if (index > -1) {
this._registrationCallbacks.splice(index, 1);
}
};
}
private _mapStringToNodeType(type: string): NodeType {
switch (type) {
case 'composite': return NodeType.Composite;
case 'decorator': return NodeType.Decorator;
case 'action': return NodeType.Action;
case 'condition': return NodeType.Condition;
default: return NodeType.Action;
}
}
private _getDefaultCategory(type: string): string {
switch (type) {
case 'composite': return '组合';
case 'decorator': return '装饰器';
case 'action': return '动作';
case 'condition': return '条件';
default: return '其他';
}
}
private _convertPropertiesToSchema(properties: NodePropertyConfig[]): Record<string, any> {
const schema: Record<string, any> = {};
for (const prop of properties) {
schema[prop.name] = {
type: this._mapPropertyType(prop.type),
default: prop.defaultValue,
description: prop.description,
min: prop.min,
max: prop.max,
options: prop.options?.map((o) => o.value)
};
}
return schema;
}
private _mapPropertyType(type: string): string {
switch (type) {
case 'string':
case 'code':
case 'blackboard':
case 'select':
return 'string';
case 'number':
return 'number';
case 'boolean':
return 'boolean';
default:
return 'string';
}
}
private _getChildrenConstraints(config: NodeRegistrationConfig): { min?: number; max?: number } | undefined {
if (config.minChildren !== undefined || config.maxChildren !== undefined) {
return {
min: config.minChildren,
max: config.maxChildren
};
}
switch (config.type) {
case 'composite':
return { min: 1 };
case 'decorator':
return { min: 1, max: 1 };
case 'action':
case 'condition':
return { max: 0 };
default:
return undefined;
}
}
private _createTemplate(config: NodeRegistrationConfig, metadata: NodeMetadata): NodeTemplate {
const defaultConfig: any = {
nodeType: config.type
};
switch (config.type) {
case 'composite':
defaultConfig.compositeType = config.implementationType;
break;
case 'decorator':
defaultConfig.decoratorType = config.implementationType;
break;
case 'action':
defaultConfig.actionType = config.implementationType;
break;
case 'condition':
defaultConfig.conditionType = config.implementationType;
break;
}
for (const prop of config.properties || []) {
if (prop.defaultValue !== undefined) {
defaultConfig[prop.name] = prop.defaultValue;
}
}
const template: NodeTemplate = {
type: metadata.nodeType,
displayName: config.displayName,
category: config.category || this._getDefaultCategory(config.type),
description: config.description || '',
icon: config.icon || this._getDefaultIcon(config.type),
color: config.color || this._getDefaultColor(config.type),
className: config.implementationType,
defaultConfig,
properties: (config.properties || []).map((p) => ({
name: p.name,
type: p.type,
label: p.label,
description: p.description,
defaultValue: p.defaultValue,
options: p.options,
min: p.min,
max: p.max,
required: p.required
}))
};
if (config.minChildren !== undefined) {
template.minChildren = config.minChildren;
template.requiresChildren = config.minChildren > 0;
}
if (config.maxChildren !== undefined) {
template.maxChildren = config.maxChildren;
}
return template;
}
private _getDefaultIcon(type: string): string {
switch (type) {
case 'composite': return 'GitBranch';
case 'decorator': return 'Settings';
case 'action': return 'Play';
case 'condition': return 'HelpCircle';
default: return 'Circle';
}
}
private _getDefaultColor(type: string): string {
switch (type) {
case 'composite': return '#1976d2';
case 'decorator': return '#fb8c00';
case 'action': return '#388e3c';
case 'condition': return '#d32f2f';
default: return '#757575';
}
}
}

View File

@@ -0,0 +1,154 @@
import { IValidator, ValidationResult, ValidationError } from '../../domain/interfaces/IValidator';
import { BehaviorTree } from '../../domain/models/BehaviorTree';
import { Node } from '../../domain/models/Node';
import { Connection } from '../../domain/models/Connection';
import { translateBT } from '../../hooks/useBTLocale';
/**
* 行为树验证器实现
* Behavior Tree Validator Implementation
*/
export class BehaviorTreeValidator implements IValidator {
/**
* 验证整个行为树
*/
validateTree(tree: BehaviorTree): ValidationResult {
const errors: ValidationError[] = [];
// 验证所有节点
for (const node of tree.nodes) {
const nodeResult = this.validateNode(node);
errors.push(...nodeResult.errors);
}
// 验证所有连接
for (const connection of tree.connections) {
const connResult = this.validateConnection(connection, tree);
errors.push(...connResult.errors);
}
// 验证循环引用
const cycleResult = this.validateNoCycles(tree);
errors.push(...cycleResult.errors);
return {
isValid: errors.length === 0,
errors
};
}
/**
* 验证节点
* Validate node
*/
validateNode(node: Node): ValidationResult {
const errors: ValidationError[] = [];
// 验证节点必填字段 | Validate required fields
if (!node.id) {
errors.push({
message: translateBT('validation.nodeIdRequired'),
nodeId: node.id
});
}
if (!node.template) {
errors.push({
message: translateBT('validation.nodeTemplateRequired'),
nodeId: node.id
});
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* 验证连接
* Validate connection
*/
validateConnection(connection: Connection, tree: BehaviorTree): ValidationResult {
const errors: ValidationError[] = [];
// 验证连接的源节点和目标节点都存在 | Validate source and target nodes exist
const fromNode = tree.nodes.find((n) => n.id === connection.from);
const toNode = tree.nodes.find((n) => n.id === connection.to);
if (!fromNode) {
errors.push({
message: translateBT('validation.sourceNodeNotFound', undefined, { nodeId: connection.from })
});
}
if (!toNode) {
errors.push({
message: translateBT('validation.targetNodeNotFound', undefined, { nodeId: connection.to })
});
}
// 不能自己连接自己 | Cannot connect to self
if (connection.from === connection.to) {
errors.push({
message: translateBT('validation.selfConnection'),
nodeId: connection.from
});
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* 验证是否会产生循环引用
* Validate no cycles exist
*/
validateNoCycles(tree: BehaviorTree): ValidationResult {
const errors: ValidationError[] = [];
const visited = new Set<string>();
const recursionStack = new Set<string>();
const hasCycle = (nodeId: string): boolean => {
if (recursionStack.has(nodeId)) {
return true;
}
if (visited.has(nodeId)) {
return false;
}
visited.add(nodeId);
recursionStack.add(nodeId);
const node = tree.nodes.find((n) => n.id === nodeId);
if (node) {
for (const childId of node.children) {
if (hasCycle(childId)) {
return true;
}
}
}
recursionStack.delete(nodeId);
return false;
};
for (const node of tree.nodes) {
if (hasCycle(node.id)) {
errors.push({
message: translateBT('validation.cycleDetected'),
nodeId: node.id
});
break;
}
}
return {
isValid: errors.length === 0,
errors
};
}
}