Feature/runtime cdn and plugin loader (#240)
* feat(ui): 完善 UI 布局系统和编辑器可视化工具 * refactor: 移除 ModuleRegistry,统一使用 PluginManager 插件系统 * fix: 修复 CodeQL 警告并提升测试覆盖率 * refactor: 分离运行时入口点,解决 runtime bundle 包含 React 的问题 * fix(ci): 添加 editor-core 和 editor-runtime 到 CI 依赖构建步骤 * docs: 完善 ServiceContainer 文档,新增 Symbol.for 模式和 @InjectProperty 说明 * fix(ci): 修复 type-check 失败问题 * fix(ci): 修复类型检查失败问题 * fix(ci): 修复类型检查失败问题 * fix(ci): behavior-tree 构建添加 @tauri-apps 外部依赖 * fix(ci): behavior-tree 添加 @tauri-apps/plugin-fs 类型依赖 * fix(ci): platform-web 添加缺失的 behavior-tree 依赖 * fix(lint): 移除正则表达式中不必要的转义字符
This commit is contained in:
@@ -1,142 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { NodeTemplate, NodeTemplates } 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;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { NodeFactory } from './NodeFactory';
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './factories';
|
||||
export * from './serialization';
|
||||
@@ -1,127 +0,0 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { BehaviorTreeSerializer } from './BehaviorTreeSerializer';
|
||||
@@ -1,264 +0,0 @@
|
||||
import { NodeTemplate, NodeMetadataRegistry, NodeMetadata, NodeType } 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* 行为树验证器实现
|
||||
*/
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证节点
|
||||
*/
|
||||
validateNode(node: Node): ValidationResult {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
// 验证节点必填字段
|
||||
if (!node.id) {
|
||||
errors.push({
|
||||
message: '节点 ID 不能为空',
|
||||
nodeId: node.id
|
||||
});
|
||||
}
|
||||
|
||||
if (!node.template) {
|
||||
errors.push({
|
||||
message: '节点模板不能为空',
|
||||
nodeId: node.id
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证连接
|
||||
*/
|
||||
validateConnection(connection: Connection, tree: BehaviorTree): ValidationResult {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
// 验证连接的源节点和目标节点都存在
|
||||
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: `连接的源节点不存在: ${connection.from}`
|
||||
});
|
||||
}
|
||||
|
||||
if (!toNode) {
|
||||
errors.push({
|
||||
message: `连接的目标节点不存在: ${connection.to}`
|
||||
});
|
||||
}
|
||||
|
||||
// 不能自己连接自己
|
||||
if (connection.from === connection.to) {
|
||||
errors.push({
|
||||
message: '节点不能连接到自己',
|
||||
nodeId: connection.from
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证是否会产生循环引用
|
||||
*/
|
||||
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: '行为树中存在循环引用',
|
||||
nodeId: node.id
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user